<persisted-output>
Output too large (28.5KB). Full output saved to: C:\Users\decid\.ccs\instances\bigscreen\projects\C--Users-decid-Documents-projects-spt-core\d05e91fb-5cdf-40e4-992c-76f8df17f251\tool-results\toolu_01FBzNGanajdWnf65jCTLnvb.txt

Preview (first 2KB):
C:\Users\decid\Documents\projects\spt-core\CONTEXT.md-472-
CONTEXT.md-473-**subnet member \ membership proof (seed-proof)**:
C:\Users\decid\Documents\projects\spt-core\CONTEXT.md-474-[Omitted long context line]
CONTEXT.md:475:_Replaces_: the pairwise-`is_trusted` authorization at every inbound gate (registry apply, WAN receive, sync, notif, connection accept).
C:\Users\decid\Documents\projects\spt-core\CONTEXT.md-476-
CONTEXT.md-477-**member roster**:
C:\Users\decid\Documents\projects\spt-core\CONTEXT.md-478-[Omitted long context line]
--
C:\Users\decid\Documents\projects\spt-core\docs\adr\0017-subnet-mesh-membership-seed-proof.md-8-
docs\adr\0017-subnet-mesh-membership-seed-proof.md-9-A subnet was, until now, the **pairing graph, not a mesh**: a node saw only the peers it had *directly* paired with. Repro (user, 2026-06-08): A creates subnet → B joins A → A offline → C joins B → B offline → A online → **A and C can never see or reach each other**. Two v1-by-design gaps caused it:
C:\Users\decid\Documents\projects\spt-core\docs\adr\0017-subnet-mesh-membership-seed-proof.md-10-
docs\adr\0017-subnet-mesh-membership-seed-proof.md:11:1. **Authorization was pairwise TOFU pinning** (`spt-store::trust`): the inbound gate is `is_trusted(subnet, origin_node)` — A pinned B, C pinned B, A never pinned C, so A rejects C's origin.
docs\adr\0017-subnet-mesh-membership-seed-proof.md-12-2. **Gossip was own-rows-only with no roster and no relay** (`registryhost::advertise_local`, `peerloop`): a node pushed only its own registry rows to its directly-paired peers, and the pairing seed transfer (`pairing::wire::Frame::Seed`) carried only `{seed, epoch}` — no member list. So A never even *learned* C exists.
C:\Users\decid\Documents\projects\spt-core\docs\adr\0017-subnet-mesh-membership-seed-proof.md-13-
C:\Users\decid\Documents\projects\spt-core\docs\adr\0017-subnet-mesh-membership-seed-proof.md-14-[Omitted long context line]
--
...
</persisted-output>

C:\Users\decid\Documents\projects\spt-core\CONTEXT.md-472-
CONTEXT.md-473-**subnet member \ membership proof (seed-proof)**:
C:\Users\decid\Documents\projects\spt-core\CONTEXT.md-474-[Omitted long context line]
CONTEXT.md:475:_Replaces_: the pairwise-`is_trusted` authorization at every inbound gate (registry apply, WAN receive, sync, notif, connection accept).
C:\Users\decid\Documents\projects\spt-core\CONTEXT.md-476-
CONTEXT.md-477-**member roster**:
C:\Users\decid\Documents\projects\spt-core\CONTEXT.md-478-[Omitted long context line]
--
C:\Users\decid\Documents\projects\spt-core\docs\adr\0017-subnet-mesh-membership-seed-proof.md-8-
docs\adr\0017-subnet-mesh-membership-seed-proof.md-9-A subnet was, until now, the **pairing graph, not a mesh**: a node saw only the peers it had *directly* paired with. Repro (user, 2026-06-08): A creates subnet → B joins A → A offline → C joins B → B offline → A online → **A and C can never see or reach each other**. Two v1-by-design gaps caused it:
C:\Users\decid\Documents\projects\spt-core\docs\adr\0017-subnet-mesh-membership-seed-proof.md-10-
docs\adr\0017-subnet-mesh-membership-seed-proof.md:11:1. **Authorization was pairwise TOFU pinning** (`spt-store::trust`): the inbound gate is `is_trusted(subnet, origin_node)` — A pinned B, C pinned B, A never pinned C, so A rejects C's origin.
docs\adr\0017-subnet-mesh-membership-seed-proof.md-12-2. **Gossip was own-rows-only with no roster and no relay** (`registryhost::advertise_local`, `peerloop`): a node pushed only its own registry rows to its directly-paired peers, and the pairing seed transfer (`pairing::wire::Frame::Seed`) carried only `{seed, epoch}` — no member list. So A never even *learned* C exists.
C:\Users\decid\Documents\projects\spt-core\docs\adr\0017-subnet-mesh-membership-seed-proof.md-13-
C:\Users\decid\Documents\projects\spt-core\docs\adr\0017-subnet-mesh-membership-seed-proof.md-14-[Omitted long context line]
--
C:\Users\decid\Documents\projects\spt-core\docs\adr\0017-subnet-mesh-membership-seed-proof.md-19-
docs\adr\0017-subnet-mesh-membership-seed-proof.md-20-**A subnet member is any node that can prove knowledge of the subnet's current-epoch seed.** Authorization moves from a stored pairwise pin to a **live, per-connection symmetric membership proof ("seed-proof")**; visibility is restored by relaying only a **member roster** (a discovery directory), never registry rows. Concretely:
C:\Users\decid\Documents\projects\spt-core\docs\adr\0017-subnet-mesh-membership-seed-proof.md-21-
docs\adr\0017-subnet-mesh-membership-seed-proof.md:22:[Omitted long matching line]
C:\Users\decid\Documents\projects\spt-core\docs\adr\0017-subnet-mesh-membership-seed-proof.md-23-
C:\Users\decid\Documents\projects\spt-core\docs\adr\0017-subnet-mesh-membership-seed-proof.md-24-[Omitted long context line]
C:\Users\decid\Documents\projects\spt-core\docs\adr\0017-subnet-mesh-membership-seed-proof.md-25-
--
C:\Users\decid\Documents\projects\spt-core\traceable-reqs.toml-275-[[requirements]]
C:\Users\decid\Documents\projects\spt-core\traceable-reqs.toml-276-id = "REQ-PAIR-2"
C:\Users\decid\Documents\projects\spt-core\traceable-reqs.toml-277-title = "Local trust store with TOFU + warn-on-change"
traceable-reqs.toml:278:[Omitted long matching line]
C:\Users\decid\Documents\projects\spt-core\traceable-reqs.toml-279-[[requirements]]
C:\Users\decid\Documents\projects\spt-core\traceable-reqs.toml-280-id = "REQ-PAIR-3"
C:\Users\decid\Documents\projects\spt-core\traceable-reqs.toml-281-title = "Fetch current pairing code from any paired node"
--
traceable-reqs.toml-751-# Minted inactive (rule 5: no required_stages until the mesh milestone executes). Design: ADR-0017 + SUBNET-MESH-PLAN.md + CONTEXT §Pairing & trust.
C:\Users\decid\Documents\projects\spt-core\traceable-reqs.toml-752-[[requirements]]
C:\Users\decid\Documents\projects\spt-core\traceable-reqs.toml-753-id = "REQ-MESH-1"
traceable-reqs.toml:754:[Omitted long matching line]
traceable-reqs.toml:755:required_stages = ["impl", "unit"]  # ACTIVATED Mesh-D1 (codec slice: MK derivation + mutual channel-bound seed-proof transcript/verify + frame codec). D2 enriches impl (connect-time handshake + ConnEntry flag), D5 enriches impl (gate swap is_trusted→is_member at the five inbound sites). unit = MK derivation; valid/forged/wrong-subnet/wrong-epoch/cross-connection-replay/mutual-fail; D5 adds gate admits-unpaired-member / rejects-non-member.
C:\Users\decid\Documents\projects\spt-core\traceable-reqs.toml-756-[[requirements]]
C:\Users\decid\Documents\projects\spt-core\traceable-reqs.toml-757-id = "REQ-MESH-2"
C:\Users\decid\Documents\projects\spt-core\traceable-reqs.toml-758-[Omitted long context line]
--
C:\Users\decid\Documents\projects\spt-core\traceable-reqs.toml-767-[Omitted long context line]
C:\Users\decid\Documents\projects\spt-core\traceable-reqs.toml-768-[[requirements]]
C:\Users\decid\Documents\projects\spt-core\traceable-reqs.toml-769-id = "REQ-MESH-5"
traceable-reqs.toml: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."
traceable-reqs.toml: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.
C:\Users\decid\Documents\projects\spt-core\traceable-reqs.toml-772-[[requirements]]
C:\Users\decid\Documents\projects\spt-core\traceable-reqs.toml-773-id = "REQ-MESH-6"
traceable-reqs.toml-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.)"
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-574-    rig_wait(
crates\spt-daemon\tests\twohost.rs-575-        "pairing: A pinned via the daemon-hosted responder",
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-576-        rig.wait,
crates\spt-daemon\tests\twohost.rs:577:        || TrustStore::load().is_trusted(&rig.subnet, &rig.a_hex()),
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-578-    );
crates\spt-daemon\tests\twohost.rs-579-    println!("TWOHOST OK: pairing (responder, daemon-hosted)");
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-580-
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-830-        "the ceremony transferred the subnet seed to the joiner (REQ-PAIR-5)"
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-831-    );
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-832-    assert!(
crates\spt-daemon\tests\twohost.rs:833:        TrustStore::load().is_trusted(&rig.subnet, &rig.b_hex()),
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-834-        "the ceremony pinned B in A's canonical trust store"
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-835-    );
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-836-
--
C:\Users\decid\Documents\projects\spt-core\SUBNET-MESH-PLAN.md-4-
C:\Users\decid\Documents\projects\spt-core\SUBNET-MESH-PLAN.md-5-## The bug it fixes
C:\Users\decid\Documents\projects\spt-core\SUBNET-MESH-PLAN.md-6-
SUBNET-MESH-PLAN.md:7:Non-directly-paired subnet members never see or reach each other — a subnet was the **pairing graph, not a mesh**. Repro (user 2026-06-08): A creates → B joins A → A offline → C joins B → B offline → A online → A and C invisible. Two gaps: pairwise-pin authorization (`trust::is_trusted`) + own-rows-only gossip with no roster/relay (`registryhost::advertise_local`, `peerloop`, `pairing::wire::Frame::Seed = {seed,epoch}`).
C:\Users\decid\Documents\projects\spt-core\SUBNET-MESH-PLAN.md-8-
C:\Users\decid\Documents\projects\spt-core\SUBNET-MESH-PLAN.md-9-## The decided architecture (one paragraph)
C:\Users\decid\Documents\projects\spt-core\SUBNET-MESH-PLAN.md-10-
--
C:\Users\decid\Documents\projects\spt-core\SUBNET-MESH-PLAN.md-12-
C:\Users\decid\Documents\projects\spt-core\SUBNET-MESH-PLAN.md-13-## Decisions (resolved in the grill — supersedes the original open-questions)
C:\Users\decid\Documents\projects\spt-core\SUBNET-MESH-PLAN.md-14-
SUBNET-MESH-PLAN.md:15:1. **Scope = full reachability**, not just visibility; the security-critical center is the **inbound-accept gate** (membership predicate replaces `is_trusted` at registry apply, WAN receive, sync, notif, connection accept). → REQ-MESH-1.
C:\Users\decid\Documents\projects\spt-core\SUBNET-MESH-PLAN.md-16-2. **Auth = symmetric seed-proof** (not per-node signatures / not row relay). `MK = HKDF(seed, domain ‖ subnet_id ‖ seed_epoch)`; mutual **channel-bound** challenge-response (binds both handshake-proven pubkeys, nonces, subnet_id, seed_epoch, role); **once per connection**, cached on the broker `ConnEntry`; keep-alive keeps connections multi-week so re-proof is restart/partition/rotation-only (no per-message tax); exact-epoch match. → REQ-MESH-1.
C:\Users\decid\Documents\projects\spt-core\SUBNET-MESH-PLAN.md-17-3. **Roster subsumes the trust store.** Authorization is live seed-proof; pairwise pinning **retired**. → REQ-MESH-5.
C:\Users\decid\Documents\projects\spt-core\SUBNET-MESH-PLAN.md-18-4. **Warn-on-change demoted** to an awareness notice anchored on **machine_id** (not label); fires the REQ-SUBNET-7 re-pair-overwrite event. → REQ-MESH-5.
SUBNET-MESH-PLAN.md:19:5. **Hard cutover** — delete `peers.json` + the `is_trusted` path, **no migration** (expendable test fleet re-pairs fresh). → REQ-MESH-5.
C:\Users\decid\Documents\projects\spt-core\SUBNET-MESH-PLAN.md-20-6. **Roster** = node-level union-merge grow-set (pubkey, label, machine_id, address, last-seen — **not** the seed), seeded in full at pairing, merged on every member connection under the lease epoch, forgery-inert, persists-through-silence. Removal via **tombstones** (a grow-set can't delete by omission). → REQ-MESH-2.
C:\Users\decid\Documents\projects\spt-core\SUBNET-MESH-PLAN.md-21-7. **Rows** stay own-authored; push target = all roster members; **no third-party relay** → 7.5/4.10 preserved. → REQ-MESH-3.
C:\Users\decid\Documents\projects\spt-core\SUBNET-MESH-PLAN.md-22-8. **Revoke = seed rotation, timeboxed.** `spt subnet revoke <node>...` (list) tombstones now + schedules **one** rotation at a coalescing window close (default 1h; further revokes coalesce → one epoch bump); `--force-rotate-seed` rotates now (compromised path); seed pushed confidentially over member connections, never in gossip; elevation-gated. → REQ-MESH-4.
--
C:\Users\decid\Documents\projects\spt-core\SUBNET-MESH-PLAN.md-54-
SUBNET-MESH-PLAN.md-55-1. **Mesh-D1 — membership key + seed-proof codec (REQ-MESH-1 impl\unit).** Pure primitive, no wiring: `MK = HKDF(seed, domain ‖ subnet_id ‖ seed_epoch)`; the mutual channel-bound challenge-response frame enc/dec + verify, reusing `pairing::transcript`. unit = MK derivation; valid / forged / wrong-subnet / wrong-`seed_epoch` / cross-connection-replay / mutual-fail. **Activate REQ-MESH-1 (impl, unit).**
C:\Users\decid\Documents\projects\spt-core\SUBNET-MESH-PLAN.md-56-
SUBNET-MESH-PLAN.md:57:[Omitted long matching line]
C:\Users\decid\Documents\projects\spt-core\SUBNET-MESH-PLAN.md-58-
C:\Users\decid\Documents\projects\spt-core\SUBNET-MESH-PLAN.md-59-[Omitted long context line]
C:\Users\decid\Documents\projects\spt-core\SUBNET-MESH-PLAN.md-60-
C:\Users\decid\Documents\projects\spt-core\SUBNET-MESH-PLAN.md-61-4. **Mesh-D4 — roster propagation (REQ-MESH-2 cont).** On-connect roster exchange/merge over member connections; addresses captured from the live connection (`conn_remote_addr`). Reconcile with REQ-CONV-1 `peer-addrs.json` (absorb or feed — decide here). int = B knows {A,C}; A connects B, A learns C (roster grows transitively). Still no mesh *reach* yet (gate + fan-out are D5/D6).
C:\Users\decid\Documents\projects\spt-core\SUBNET-MESH-PLAN.md-62-
SUBNET-MESH-PLAN.md:63:[Omitted long matching line]
C:\Users\decid\Documents\projects\spt-core\SUBNET-MESH-PLAN.md-64-
SUBNET-MESH-PLAN.md-65-6. **Mesh-D6 — fan-out widen → MESH ON (REQ-MESH-3).** Change the registry push target from directly-paired peers to **all roster members** (`peerloop`\`advertise_local`); rows stay own-authored (no relay). **The D0 harness flips green** (A↔C converge). Regression assert: every applied row's author == its handshake origin (no third-party row ever applied). **Activate REQ-MESH-3 (impl, unit, int)** — D0 becomes its int evidence.
C:\Users\decid\Documents\projects\spt-core\SUBNET-MESH-PLAN.md-66-
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\notifsync.rs-57-    /// fail-closed on both axes.
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\notifsync.rs-58-    // [impl->REQ-NOTIF-1]
crates\spt-daemon\src\notifsync.rs-59-    pub fn admits(&self, subnet: &str, origin_node: &str) -> bool {
crates\spt-daemon\src\notifsync.rs:60:        self.subnets.iter().any(|s| s == subnet) && self.trust.is_trusted(subnet, origin_node)
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\notifsync.rs-61-    }
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\notifsync.rs-62-}
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\notifsync.rs-63-
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\notifsync.rs-103-            if !policy.subnets.iter().any(|s| s == &row.subnet) {
crates\spt-daemon\src\notifsync.rs-104-                return Ok(NotifApplyVerdict::DroppedNonMember);
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\notifsync.rs-105-            }
crates\spt-daemon\src\notifsync.rs:106:            if !policy.trust.is_trusted(&row.subnet, origin_node) {
crates\spt-daemon\src\notifsync.rs-107-                return Ok(NotifApplyVerdict::DroppedUntrusted);
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\notifsync.rs-108-            }
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\notifsync.rs-109-            let outcome = store
--
crates\spt-daemon\src\sync.rs-99-    pub fn allows(&self, branch: &str, origin_node: &str) -> bool {
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\sync.rs-100-        if let Some(id) = branch.strip_prefix("a-") {
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\sync.rs-101-            self.subnets.iter().any(|(s, hide)| {
crates\spt-daemon\src\sync.rs:102:                self.trust.is_trusted(s, origin_node) && self.vis.synced(id, s, *hide)
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\sync.rs-103-            })
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\sync.rs-104-        } else if branch.strip_prefix("p-").is_some() {
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\sync.rs-105-            self.subnets
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\sync.rs-106-                .iter()
crates\spt-daemon\src\sync.rs:107:                .any(|(s, _)| self.trust.is_trusted(s, origin_node))
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\sync.rs-108-        } else {
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\sync.rs-109-            false // machinery refs are never sync-served
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\sync.rs-110-        }
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-78-
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-79-    /// member subnet ∧ origin trusted in that subnet — fail-closed on both.
crates\spt-daemon\src\registryhost.rs-80-    pub fn admits(&self, subnet: &str, origin_node: &str) -> bool {
crates\spt-daemon\src\registryhost.rs:81:        self.subnets.iter().any(|s| s == subnet) && self.trust.is_trusted(subnet, origin_node)
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-82-    }
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-83-}
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-84-
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-225-                if !policy.subnets.iter().any(|s| s == &u.subnet) {
crates\spt-daemon\src\registryhost.rs-226-                    return RegistryApplyVerdict::NotMember;
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-227-                }
crates\spt-daemon\src\registryhost.rs:228:                if !policy.trust.is_trusted(&u.subnet, origin_node) {
crates\spt-daemon\src\registryhost.rs-229-                    return RegistryApplyVerdict::UntrustedOrigin;
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-230-                }
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-231-                admitted_any = true;
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-288-                if !policy.subnets.iter().any(|s| s == &u.subnet) {
crates\spt-daemon\src\registryhost.rs-289-                    return RegistryApplyVerdict::NotMember;
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-290-                }
crates\spt-daemon\src\registryhost.rs:291:                if !policy.trust.is_trusted(&u.subnet, origin_node) {
crates\spt-daemon\src\registryhost.rs-292-                    return RegistryApplyVerdict::UntrustedOrigin;
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-293-                }
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-294-                admitted_any = true;
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-896-        repair_evict_superseded(&mut trust, "home", "new-key", &intro, &snap_dir);
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-897-
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-898-        assert!(
crates\spt-daemon\src\registryhost.rs:899:            !trust.is_trusted("home", "old-key"),
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-900-            "ceremony subnet's trust row dropped"
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-901-        );
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-902-        assert!(
crates\spt-daemon\src\registryhost.rs:903:            trust.is_trusted("worknet", "old-key"),
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-904-            "other subnets untouched — the ceremony proved THIS subnet's code only"
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-905-        );
crates\spt-daemon\src\registryhost.rs-906-        let on_disk: SubnetRegistry = serde_json::from_str(
--
crates\spt-daemon\src\registryhost.rs-930-            machine_id: "mid-x".into(),
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-931-        };
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-932-        repair_evict_superseded(&mut t2, "home", "new-key", &other, &snap_dir);
crates\spt-daemon\src\registryhost.rs:933:        assert!(t2.is_trusted("home", "unrelated"));
crates\spt-daemon\src\registryhost.rs-934-        assert!(!RegistryHost::repair_evict_path(&snap_dir).exists());
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-935-    }
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-936-
--
MESH-D0-PLAN.md-7-The grill repro — *A creates → B joins A → A offline → C joins B → B offline → A online → A and C invisible* — is two own-rows-only\pairwise facts:
C:\Users\decid\Documents\projects\spt-core\MESH-D0-PLAN.md-8-
MESH-D0-PLAN.md-9-1. **Push target is pairwise.** `peerloop.rs:356` — `for peer in trust.peers_in(&sub.name)`. A node advertises its own rows to *directly-paired* peers only. No relay, no roster. (Doc line 11: "no transitive gossip in v1: trust is pairwise per subnet.") → widens to **all roster members** at **D6**.
MESH-D0-PLAN.md:10:2. **Apply gate is pairwise.** `registryhost.rs:228,291` — `if !policy.trust.is_trusted(&u.subnet, origin_node) { reject }`. Even if a row from a non-paired member arrived, the receiver drops it. → swaps to the **seed-proof membership** flag at **D5**.
C:\Users\decid\Documents\projects\spt-core\MESH-D0-PLAN.md-11-
MESH-D0-PLAN.md-12-C joins B, so C's trust = {B}; A's trust = {B}. **A and C never pin each other.** Under (1) C's pump never targets A; under (2) A would reject C's row anyway. Invisible + unreachable. The mesh: C learns A from B's roster at the join ceremony (D3), so when A returns C dials A *directly* and A's seed-proof gate admits it (D5) — no B in the loop. D6 widens C's push target to the roster so the row actually flows.
C:\Users\decid\Documents\projects\spt-core\MESH-D0-PLAN.md-13-
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\mesh.rs-3-//! A subnet member is a node that can prove *current-epoch seed knowledge* on a
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\mesh.rs-4-//! connection — symmetric, channel-bound, and crucially **without either side
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\mesh.rs-5-//! having pre-pinned the other** (the mesh property that the pairwise
crates\spt-net\src\net\mesh.rs:6://! `is_trusted` gate lacks). [`seedproof`] is the crypto that proof rests on:
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\mesh.rs-7-//! the membership key `MK = HKDF(seed, …)` and the mutual challenge-response
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\mesh.rs-8-//! transcript. Pure primitives here (Mesh-D1) — the connect-time handshake that
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\mesh.rs-9-//! drives them onto a live connection is Mesh-D2; the gate swap that consults
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\pairing\wire.rs-887-        assert_eq!(resp_outcome.subnet, "home");
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\pairing\wire.rs-888-        assert!(resp_outcome.intro.is_none(), "no intro sent");
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\pairing\wire.rs-889-        // Each side trusts the OTHER's real pubkey within the subnet.
crates\spt-net\src\net\pairing\wire.rs:890:        assert!(init_trust.is_trusted("home", &resp_pub.to_hex()));
crates\spt-net\src\net\pairing\wire.rs:891:        assert!(resp_trust.is_trusted("home", &init_pub.to_hex()));
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\pairing\wire.rs-892-        assert!(
crates\spt-net\src\net\pairing\wire.rs:893:            !init_trust.is_trusted("home", &init_pub.to_hex()),
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\pairing\wire.rs-894-            "self not pinned"
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\pairing\wire.rs-895-        );
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\pairing\wire.rs-896-        // [unit->REQ-PAIR-5] the joiner received the subnet seed (it is now a full member /
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-148-    /// Whether `pubkey_hex` is a trusted peer in `subnet` — the connect-time gate (an
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-149-    /// inbound peer the runtime hasn't pinned is not one of the user's own nodes).
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-150-    // [impl->REQ-PAIR-2]
crates\spt-store\src\trust.rs:151:    pub fn is_trusted(&self, subnet: &str, pubkey_hex: &str) -> bool {
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-152-        self.peers
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-153-            .iter()
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-154-            .any(|p| p.subnet == subnet && p.pubkey_hex == pubkey_hex)
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-177-    ) -> TrustOutcome {
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-178-        // Exact identity already trusted → idempotent (a key isn't "changed" against
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-179-        // itself, even when a label is present).
crates\spt-store\src\trust.rs:180:        if self.is_trusted(subnet, pubkey_hex) {
crates\spt-store\src\trust.rs-181-            return TrustOutcome::AlreadyTrusted;
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-182-        }
crates\spt-store\src\trust.rs-183-        \ Warn-on-change: a label already bound to a DIFFERENT key in this scope.
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-253-    // trusted; re-recording the SAME key is idempotent (AlreadyTrusted), not a
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-254-    // duplicate — the post-ceremony accept path.
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-255-    #[test]
crates\spt-store\src\trust.rs:256:    fn first_use_pins_then_is_trusted_idempotently() {
crates\spt-store\src\trust.rs-257-        let mut store = TrustStore::default();
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-258-        assert_eq!(
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-259-            store.record("home", "aa11", None, None),
crates\spt-store\src\trust.rs-260-            TrustOutcome::Pinned
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-261-        );
crates\spt-store\src\trust.rs:262:        assert!(store.is_trusted("home", "aa11"));
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-263-        assert_eq!(
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-264-            store.record("home", "aa11", None, None),
crates\spt-store\src\trust.rs-265-            TrustOutcome::AlreadyTrusted,
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-275-    fn trust_is_scoped_per_subnet() {
crates\spt-store\src\trust.rs-276-        let mut store = TrustStore::default();
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-277-        store.record("home", "aa11", None, None);
crates\spt-store\src\trust.rs:278:        assert!(store.is_trusted("home", "aa11"));
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-279-        assert!(
crates\spt-store\src\trust.rs:280:            !store.is_trusted("work", "aa11"),
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-281-            "not trusted in other subnet"
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-282-        );
crates\spt-store\src\trust.rs:283:        assert!(!store.is_trusted("home", "bb22"), "unknown key untrusted");
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-284-    }
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-285-
crates\spt-store\src\trust.rs-286-    \ [unit->REQ-PAIR-2] WARN-ON-CHANGE: a labelled peer that later presents a
--
crates\spt-store\src\trust.rs-297-                existing_pubkey_hex: "aa11".into()
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-298-            }
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-299-        );
crates\spt-store\src\trust.rs:300:        assert!(store.is_trusted("home", "aa11"), "old key untouched");
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-301-        assert!(
crates\spt-store\src\trust.rs:302:            !store.is_trusted("home", "bb22"),
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-303-            "new key NOT auto-trusted"


<persisted-output>
Output too large (20.4KB). Full output saved to: C:\Users\decid\.ccs\instances\bigscreen\projects\C--Users-decid-Documents-projects-spt-core\d05e91fb-5cdf-40e4-992c-76f8df17f251\tool-results\toolu_015SijyK3Fwmb3s5HMT8om9m.txt

Preview (first 2KB):
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-573-    // [int->REQ-PAIR-1] [int->REQ-SUBNET-2]
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-574-    rig_wait(
crates\spt-daemon\tests\twohost.rs-575-        "pairing: A pinned via the daemon-hosted responder",
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-576-        rig.wait,
crates\spt-daemon\tests\twohost.rs:577:        || TrustStore::load().is_trusted(&rig.subnet, &rig.a_hex()),
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-578-    );
crates\spt-daemon\tests\twohost.rs-579-    println!("TWOHOST OK: pairing (responder, daemon-hosted)");
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-580-
crates\spt-daemon\tests\twohost.rs-581-    \ ── Production loops: dispatcher (serves A's pulls/feeds) + pump (pushes
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-829-        Some(*rig.totp.as_bytes()),
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-830-        "the ceremony transferred the subnet seed to the joiner (REQ-PAIR-5)"
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-831-    );
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-832-    assert!(
crates\spt-daemon\tests\twohost.rs:833:        TrustStore::load().is_trusted(&rig.subnet, &rig.b_hex()),
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-834-        "the ceremony pinned B in A's canonical trust store"
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-835-    );
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-836-
crates\spt-daemon\tests\twohost.rs-837-    \ ── A's local seeds: perch (registry-advertised), mind (B pulls it),
--
...
</persisted-output>

C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-573-    // [int->REQ-PAIR-1] [int->REQ-SUBNET-2]
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-574-    rig_wait(
crates\spt-daemon\tests\twohost.rs-575-        "pairing: A pinned via the daemon-hosted responder",
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-576-        rig.wait,
crates\spt-daemon\tests\twohost.rs:577:        || TrustStore::load().is_trusted(&rig.subnet, &rig.a_hex()),
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-578-    );
crates\spt-daemon\tests\twohost.rs-579-    println!("TWOHOST OK: pairing (responder, daemon-hosted)");
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-580-
crates\spt-daemon\tests\twohost.rs-581-    \ ── Production loops: dispatcher (serves A's pulls/feeds) + pump (pushes
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-829-        Some(*rig.totp.as_bytes()),
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-830-        "the ceremony transferred the subnet seed to the joiner (REQ-PAIR-5)"
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-831-    );
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-832-    assert!(
crates\spt-daemon\tests\twohost.rs:833:        TrustStore::load().is_trusted(&rig.subnet, &rig.b_hex()),
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-834-        "the ceremony pinned B in A's canonical trust store"
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-835-    );
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-836-
crates\spt-daemon\tests\twohost.rs-837-    \ ── A's local seeds: perch (registry-advertised), mind (B pulls it),
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\pairing\wire.rs-886-        assert_eq!(resp_outcome.joiner_hex, init_pub.to_hex());
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\pairing\wire.rs-887-        assert_eq!(resp_outcome.subnet, "home");
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\pairing\wire.rs-888-        assert!(resp_outcome.intro.is_none(), "no intro sent");
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\pairing\wire.rs-889-        // Each side trusts the OTHER's real pubkey within the subnet.
crates\spt-net\src\net\pairing\wire.rs:890:        assert!(init_trust.is_trusted("home", &resp_pub.to_hex()));
crates\spt-net\src\net\pairing\wire.rs:891:        assert!(resp_trust.is_trusted("home", &init_pub.to_hex()));
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\pairing\wire.rs-892-        assert!(
crates\spt-net\src\net\pairing\wire.rs:893:            !init_trust.is_trusted("home", &init_pub.to_hex()),
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\pairing\wire.rs-894-            "self not pinned"
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\pairing\wire.rs-895-        );
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\pairing\wire.rs-896-        // [unit->REQ-PAIR-5] the joiner received the subnet seed (it is now a full member /
crates\spt-net\src\net\pairing\wire.rs-897-        \ future seed-holder): same name, seed, and the responder's epoch (3).
--
crates\spt-daemon\src\notifsync.rs-56-    \ is admitted: member subnet ∧ origin trusted **in that subnet** —
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\notifsync.rs-57-    /// fail-closed on both axes.
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\notifsync.rs-58-    // [impl->REQ-NOTIF-1]
crates\spt-daemon\src\notifsync.rs-59-    pub fn admits(&self, subnet: &str, origin_node: &str) -> bool {
crates\spt-daemon\src\notifsync.rs:60:        self.subnets.iter().any(|s| s == subnet) && self.trust.is_trusted(subnet, origin_node)
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\notifsync.rs-61-    }
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\notifsync.rs-62-}
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\notifsync.rs-63-
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\notifsync.rs-64-/// What the apply did with one inbound record — loud + typed, never silent.
--
crates\spt-daemon\src\notifsync.rs-102-            let NotifRecord::Row { row } = record;
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\notifsync.rs-103-            if !policy.subnets.iter().any(|s| s == &row.subnet) {
crates\spt-daemon\src\notifsync.rs-104-                return Ok(NotifApplyVerdict::DroppedNonMember);
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\notifsync.rs-105-            }
crates\spt-daemon\src\notifsync.rs:106:            if !policy.trust.is_trusted(&row.subnet, origin_node) {
crates\spt-daemon\src\notifsync.rs-107-                return Ok(NotifApplyVerdict::DroppedUntrusted);
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\notifsync.rs-108-            }
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\notifsync.rs-109-            let outcome = store
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\notifsync.rs-110-                .merge_row(row)
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-77-    }
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-78-
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-79-    /// member subnet ∧ origin trusted in that subnet — fail-closed on both.
crates\spt-daemon\src\registryhost.rs-80-    pub fn admits(&self, subnet: &str, origin_node: &str) -> bool {
crates\spt-daemon\src\registryhost.rs:81:        self.subnets.iter().any(|s| s == subnet) && self.trust.is_trusted(subnet, origin_node)
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-82-    }
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-83-}
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-84-
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-85-/// The daemon's per-subnet registry instances + this node's advertisement
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-224-            .map(|u| {
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-225-                if !policy.subnets.iter().any(|s| s == &u.subnet) {
crates\spt-daemon\src\registryhost.rs-226-                    return RegistryApplyVerdict::NotMember;
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-227-                }
crates\spt-daemon\src\registryhost.rs:228:                if !policy.trust.is_trusted(&u.subnet, origin_node) {
crates\spt-daemon\src\registryhost.rs-229-                    return RegistryApplyVerdict::UntrustedOrigin;
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-230-                }
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-231-                admitted_any = true;
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-232-                let reg = regs.entry(u.subnet.clone()).or_default();
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-287-            .map(|u| {
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-288-                if !policy.subnets.iter().any(|s| s == &u.subnet) {
crates\spt-daemon\src\registryhost.rs-289-                    return RegistryApplyVerdict::NotMember;
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-290-                }
crates\spt-daemon\src\registryhost.rs:291:                if !policy.trust.is_trusted(&u.subnet, origin_node) {
crates\spt-daemon\src\registryhost.rs-292-                    return RegistryApplyVerdict::UntrustedOrigin;
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-293-                }
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-294-                admitted_any = true;
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-295-                let reg = regs.entry(u.subnet.clone()).or_default();
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-895-        };
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-896-        repair_evict_superseded(&mut trust, "home", "new-key", &intro, &snap_dir);
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-897-
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-898-        assert!(
crates\spt-daemon\src\registryhost.rs:899:            !trust.is_trusted("home", "old-key"),
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-900-            "ceremony subnet's trust row dropped"
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-901-        );
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-902-        assert!(
crates\spt-daemon\src\registryhost.rs:903:            trust.is_trusted("worknet", "old-key"),
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-904-            "other subnets untouched — the ceremony proved THIS subnet's code only"
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-905-        );
crates\spt-daemon\src\registryhost.rs-906-        let on_disk: SubnetRegistry = serde_json::from_str(
crates\spt-daemon\src\registryhost.rs-907-            &std::fs::read_to_string(RegistryHost::snapshot_path(&snap_dir, "home")).unwrap(),
--
crates\spt-daemon\src\registryhost.rs-929-            label: "ELSEWHERE".into(),
crates\spt-daemon\src\registryhost.rs-930-            machine_id: "mid-x".into(),
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-931-        };
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-932-        repair_evict_superseded(&mut t2, "home", "new-key", &other, &snap_dir);
crates\spt-daemon\src\registryhost.rs:933:        assert!(t2.is_trusted("home", "unrelated"));
crates\spt-daemon\src\registryhost.rs-934-        assert!(!RegistryHost::repair_evict_path(&snap_dir).exists());
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-935-    }
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\registryhost.rs-936-
crates\spt-daemon\src\registryhost.rs-937-    \ [unit->REQ-INST-7] inbound feeds gate fail-closed: non-member subnet
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\sync.rs-98-    // [impl->REQ-INST-13]
crates\spt-daemon\src\sync.rs-99-    pub fn allows(&self, branch: &str, origin_node: &str) -> bool {
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\sync.rs-100-        if let Some(id) = branch.strip_prefix("a-") {
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\sync.rs-101-            self.subnets.iter().any(|(s, hide)| {
crates\spt-daemon\src\sync.rs:102:                self.trust.is_trusted(s, origin_node) && self.vis.synced(id, s, *hide)
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\sync.rs-103-            })
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\sync.rs-104-        } else if branch.strip_prefix("p-").is_some() {
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\sync.rs-105-            self.subnets
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\sync.rs-106-                .iter()
crates\spt-daemon\src\sync.rs:107:                .any(|(s, _)| self.trust.is_trusted(s, origin_node))
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\sync.rs-108-        } else {
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\sync.rs-109-            false // machinery refs are never sync-served
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\sync.rs-110-        }
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\sync.rs-111-    }
--
crates\spt-store\src\trust.rs-176-        user: Option<&str>,
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-177-    ) -> TrustOutcome {
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-178-        // Exact identity already trusted → idempotent (a key isn't "changed" against
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-179-        // itself, even when a label is present).
crates\spt-store\src\trust.rs:180:        if self.is_trusted(subnet, pubkey_hex) {
crates\spt-store\src\trust.rs-181-            return TrustOutcome::AlreadyTrusted;
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-182-        }
crates\spt-store\src\trust.rs-183-        \ Warn-on-change: a label already bound to a DIFFERENT key in this scope.
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-184-        if let Some(lbl) = label {
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-258-        assert_eq!(
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-259-            store.record("home", "aa11", None, None),
crates\spt-store\src\trust.rs-260-            TrustOutcome::Pinned
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-261-        );
crates\spt-store\src\trust.rs:262:        assert!(store.is_trusted("home", "aa11"));
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-263-        assert_eq!(
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-264-            store.record("home", "aa11", None, None),
crates\spt-store\src\trust.rs-265-            TrustOutcome::AlreadyTrusted,
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-266-            "re-pair with same key is idempotent"
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-274-    #[test]
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-275-    fn trust_is_scoped_per_subnet() {
crates\spt-store\src\trust.rs-276-        let mut store = TrustStore::default();
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-277-        store.record("home", "aa11", None, None);
crates\spt-store\src\trust.rs:278:        assert!(store.is_trusted("home", "aa11"));
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-279-        assert!(
crates\spt-store\src\trust.rs:280:            !store.is_trusted("work", "aa11"),
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-281-            "not trusted in other subnet"
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-282-        );
crates\spt-store\src\trust.rs:283:        assert!(!store.is_trusted("home", "bb22"), "unknown key untrusted");
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-284-    }
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-285-
crates\spt-store\src\trust.rs-286-    \ [unit->REQ-PAIR-2] WARN-ON-CHANGE: a labelled peer that later presents a
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-287-    // DIFFERENT key returns KeyChanged carrying the existing key, and does NOT mutate
--
crates\spt-store\src\trust.rs-296-            TrustOutcome::KeyChanged {
crates\spt-store\src\trust.rs-297-                existing_pubkey_hex: "aa11".into()
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-298-            }
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-299-        );
crates\spt-store\src\trust.rs:300:        assert!(store.is_trusted("home", "aa11"), "old key untouched");
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-301-        assert!(
crates\spt-store\src\trust.rs:302:            !store.is_trusted("home", "bb22"),
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-303-            "new key NOT auto-trusted"
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-304-        );
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-305-        assert_eq!(store.peers.len(), 1, "no row added on a warned change");
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-306-    }
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-312-    fn repin_applies_accepted_key_change() {
crates\spt-store\src\trust.rs-313-        let mut store = TrustStore::default();
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-314-        store.record("home", "aa11", Some("laptop"), None);
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-315-        store.repin("home", "laptop", "bb22", None).expect("repin");
crates\spt-store\src\trust.rs:316:        assert!(store.is_trusted("home", "bb22"), "new key trusted");
crates\spt-store\src\trust.rs:317:        assert!(!store.is_trusted("home", "aa11"), "old key replaced");
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-318-        assert_eq!(
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-319-            store.repin("home", "ghost", "cc33", None).unwrap_err(),
crates\spt-store\src\trust.rs-320-            TrustError::NotFound("home/ghost".into())
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-321-        );
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-335-            store.record("home", "bb22", None, None),
crates\spt-store\src\trust.rs-336-            TrustOutcome::Pinned
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-337-        );
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-338-        assert_eq!(store.peers.len(), 2);
crates\spt-store\src\trust.rs:339:        assert!(store.is_trusted("home", "aa11") && store.is_trusted("home", "bb22"));
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-340-    }
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-341-
crates\spt-store\src\trust.rs-342-    \ [unit->REQ-PAIR-2] revocation is a trust-delete: a revoked peer is no longer
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-343-    // trusted; a second revoke is a no-op false. (Pairs with seed rotation for real
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-346-    fn revoke_removes_trust() {
crates\spt-store\src\trust.rs-347-        let mut store = TrustStore::default();
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-348-        store.record("home", "aa11", None, None);
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-349-        assert!(store.revoke("home", "aa11"));
crates\spt-store\src\trust.rs:350:        assert!(!store.is_trusted("home", "aa11"));
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-351-        assert!(
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-352-            !store.revoke("home", "aa11"),
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-353-            "second revoke is no-op false"
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-354-        );
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-396-        store.save_to(&path).unwrap();
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-397-
crates\spt-store\src\trust.rs-398-        let reloaded = TrustStore::load_from(&path);
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-399-        assert_eq!(reloaded.peers.len(), 2, "multi-subnet preserved");
crates\spt-store\src\trust.rs:400:        assert!(reloaded.is_trusted("home", "dd44"), "repin durable");
crates\spt-store\src\trust.rs:401:        assert!(reloaded.is_trusted("work", "cc33"));
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-402-
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-403-        // Absent + corrupt → empty, never a panic.
crates\spt-store\src\trust.rs-404-        assert!(TrustStore::load_from(&dir.path().join("nope.json"))
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\trust.rs-405-            .peers


Bash

grep -rn "\.is_trusted(" --include=*.rs crates --exclude=twohost.rs | grep -v "/tests/" | grep -v "#\[cfg(test)\]"

Find production is_trusted call sites


crates/spt-daemon/src/notifsync.rs:60:        self.subnets.iter().any(|s| s == subnet) && self.trust.is_trusted(subnet, origin_node)
crates/spt-daemon/src/notifsync.rs:106:            if !policy.trust.is_trusted(&row.subnet, origin_node) {
crates/spt-daemon/src/registryhost.rs:81:        self.subnets.iter().any(|s| s == subnet) && self.trust.is_trusted(subnet, origin_node)
crates/spt-daemon/src/registryhost.rs:228:                if !policy.trust.is_trusted(&u.subnet, origin_node) {
crates/spt-daemon/src/registryhost.rs:291:                if !policy.trust.is_trusted(&u.subnet, origin_node) {
crates/spt-daemon/src/registryhost.rs:899:            !trust.is_trusted("home", "old-key"),
crates/spt-daemon/src/registryhost.rs:903:            trust.is_trusted("worknet", "old-key"),
crates/spt-daemon/src/registryhost.rs:933:        assert!(t2.is_trusted("home", "unrelated"));
crates/spt-daemon/src/sync.rs:102:                self.trust.is_trusted(s, origin_node) && self.vis.synced(id, s, *hide)
crates/spt-daemon/src/sync.rs:107:                .any(|(s, _)| self.trust.is_trusted(s, origin_node))
crates/spt-net/src/net/pairing/wire.rs:890:        assert!(init_trust.is_trusted("home", &resp_pub.to_hex()));
crates/spt-net/src/net/pairing/wire.rs:891:        assert!(resp_trust.is_trusted("home", &init_pub.to_hex()));
crates/spt-net/src/net/pairing/wire.rs:893:            !init_trust.is_trusted("home", &init_pub.to_hex()),
crates/spt-store/src/trust.rs:180:        if self.is_trusted(subnet, pubkey_hex) {
crates/spt-store/src/trust.rs:262:        assert!(store.is_trusted("home", "aa11"));
crates/spt-store/src/trust.rs:278:        assert!(store.is_trusted("home", "aa11"));
crates/spt-store/src/trust.rs:280:            !store.is_trusted("work", "aa11"),
crates/spt-store/src/trust.rs:283:        assert!(!store.is_trusted("home", "bb22"), "unknown key untrusted");
crates/spt-store/src/trust.rs:300:        assert!(store.is_trusted("home", "aa11"), "old key untouched");
crates/spt-store/src/trust.rs:302:            !store.is_trusted("home", "bb22"),
crates/spt-store/src/trust.rs:316:        assert!(store.is_trusted("home", "bb22"), "new key trusted");
crates/spt-store/src/trust.rs:317:        assert!(!store.is_trusted("home", "aa11"), "old key replaced");
crates/spt-store/src/trust.rs:339:        assert!(store.is_trusted("home", "aa11") && store.is_trusted("home", "bb22"));
crates/spt-store/src/trust.rs:350:        assert!(!store.is_trusted("home", "aa11"));
crates/spt-store/src/trust.rs:400:        assert!(reloaded.is_trusted("home", "dd44"), "repin durable");
crates/spt-store/src/trust.rs:401:        assert!(reloaded.is_trusted("work", "cc33"));


Now I have the full picture. Critical finding: the **gates are per-`(subnet, origin)`, evaluated per registry-row/notif/sync-item** (notifsync.rs:60/106, registryhost.rs:81/228/291, sync.rs:102/107) — not per-connection. One QUIC conn (deduped per peer pair by iroh) carries items for *every* subnet the two nodes share. So the cached verdict D5 will consume must be a **set of proven subnets per connection**, not a bool. But D1's `Challenge` frame carries only a nonce — no subnet_id, no way to negotiate *which* subnet(s). That's the real D2 fork.

Let me confirm one thing — that a single conn is shared across subnets (no per-subnet dial context) — by checking who calls `dial`/`peerloop`.


C:\Users\decid\Documents\projects\spt-core\crates\xtask\src\main.rs-924-        }
C:\Users\decid\Documents\projects\spt-core\crates\xtask\src\main.rs-925-    };
crates\xtask\src\main.rs:926:    let resolver = spt_daemon::peerloop::production_resolver();
crates\xtask\src\main.rs-927-    let seeds = spt_store::peeraddrs::PeerAddrStore::load();
C:\Users\decid\Documents\projects\spt-core\crates\xtask\src\main.rs-928-
--
crates\spt\src\wansend.rs-23-use spt_daemon::brain::Brain;
crates\spt\src\wansend.rs-24-use spt_daemon::endpoint::broker_socket_name;
crates\spt\src\wansend.rs:25:use spt_net::net::endpoint::addr_for_node_hex;
crates\spt\src\wansend.rs-26-use spt_net::net::ndjson;
crates\spt\src\wansend.rs-27-use spt_net::net::registry::{
--
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs-77-        body,
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs-78-        &op_id,
crates\spt\src\wansend.rs:79:        |hex| addr_for_node_hex(hex).and_then(|a| serde_json::to_value(a).ok()),
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs-80-    )
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs-81-}
--
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs-205-        &op_id,
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs-206-        epoch,
crates\spt\src\wansend.rs:207:        |hex| addr_for_node_hex(hex).and_then(|a| serde_json::to_value(a).ok()),
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs-208-    )
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs-209-}
--
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs-331-        &op_id,
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs-332-        epoch,
crates\spt\src\wansend.rs:333:        |hex| addr_for_node_hex(hex).and_then(|a| serde_json::to_value(a).ok()),
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs-334-    )
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs-335-}
--
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs-425-        return false;
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs-426-    };
crates\spt\src\wansend.rs:427:    let Some(addr) = addr_for_node_hex(node_hex).and_then(|a| serde_json::to_value(a).ok()) else {
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs-428-        return false;
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs-429-    };
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\broker.rs-591-            None => {
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\broker.rs-592-                // Direct (non-journaled) dial — no dedup, like a no-op_id input.
crates\spt-daemon\src\broker.rs:593:                let (conn_id, remote) = host.dial(addr).map_err(|e| e.to_string())?;
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\broker.rs-594-                let observed = host.conn_remote_addr_json(conn_id);
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\broker.rs-595-                send_frame(
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\broker.rs-603-                    .journal
crates\spt-daemon\src\broker.rs-604-                    .apply_once((NET_EFFECT_SESSION, op), EffectKind::NetDial, || {
crates\spt-daemon\src\broker.rs:605:                        let (conn_id, _) = host.dial(addr)?;
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\broker.rs-606-                        host.record_dial_op(op, conn_id);
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\broker.rs-607-                        Ok(())
--
crates\spt-daemon\tests\twohost.rs-63-use spt_daemon::dispatch::{run_dispatch_loop, DispatchPaths};
crates\spt-daemon\tests\twohost.rs-64-use spt_daemon::nethost::{NetConfig, NetHost};
crates\spt-daemon\tests\twohost.rs:65:use spt_daemon::peerloop::{
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-66-    run_peer_pump, PeerResolver, PumpCadence, PumpConfig, PumpHooks, PumpPaths,
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-67-};
--
crates\spt-daemon\tests\twohost.rs-71-use spt_daemon::update::BROKER_RESOURCE_ABI;
crates\spt-daemon\tests\twohost.rs-72-use spt_net::net::endpoint::{
crates\spt-daemon\tests\twohost.rs:73:    addr_for_node_hex, BindScope, EndpointAddr, LocalDiscovery, RelayPolicy,
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-74-};
crates\spt-daemon\tests\twohost.rs-75-use spt_net::net::pairing::totp::TotpSeed;
--

[Showing results with pagination = limit: 60]


Confirmed: `dial(addr)` (broker.rs:593/605) takes only an address — connections are **not** subnet-scoped; one conn per peer carries all shared subnets. So the proven-verdict must be a **set of subnets per ConnEntry**. The architecture itself determines the shape (single-subnet-per-conn would need *more* plumbing, not less) — no user fork needed; I'll decide it. Let me check REQ-MESH-1's current stages in the registry.


grep -n "MESH" traceable-reqs.toml | head -40

Find MESH req entries in registry


751:# Minted inactive (rule 5: no required_stages until the mesh milestone executes). Design: ADR-0017 + SUBNET-MESH-PLAN.md + CONTEXT §Pairing & trust.
753:id = "REQ-MESH-1"
757:id = "REQ-MESH-2"
761:id = "REQ-MESH-3"
765:id = "REQ-MESH-4"
769:id = "REQ-MESH-5"
773:id = "REQ-MESH-6"


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"]  # ACTIVATED Mesh-D1 (codec slice: MK derivation + mutual channel-bound seed-proof transcript/verify + frame codec). D2 enriches impl (connect-time handshake + ConnEntry flag), D5 enriches impl (gate swap is_trusted→is_member at the five inbound sites). unit = MK derivation; valid/forged/wrong-subnet/wrong-epoch/cross-connection-replay/mutual-fail; D5 adds gate admits-unpaired-member / rejects-non-member.
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 = []  # activate at mesh execution. impl = roster store (union-merge + lease, atomic, corrupt-degrades-empty) + pairing-seed full-roster transfer (Frame::Seed gains the roster) + on-connect roster exchange/merge + tombstone propagation + admission suppression. unit = merge lease (strictly-greater self-entry) + union convergence + tombstone dominates/suppresses-reinsert/clears-on-repair + persist-through-silence + serde additive.
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).
776

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

# ── Subnet full-mesh: membership seed-proof + roster-only relay (ADR-0017, 2026-06-08 grill) ──
# Minted inactive (rule 5: no required_stages until the mesh milestone executes). Design: ADR-0017 + SUBNET-MESH-PLAN.md + CONTEXT §Pairing & trust.
[[requirements]]
id = "REQ-MESH-1"
title = "Membership proof (seed-proof): symmetric current-epoch seed-knowledge replaces is_trusted at EVERY inbound gate (registry apply, WAN receive, sync, notif, connection accept). MK = HKDF(seed, domain ‖ subnet_id ‖ seed_epoch); mutual channel-bound challenge-response at connect (transcript binds both handshake-proven node pubkeys, both nonces, subnet_id, seed_epoch, role); verified once per connection, cached on the broker ConnEntry, kept warm via QUIC keep-alive so re-proof is restart/partition/rotation-only. Exact-epoch match (re-seed is the sole N-1 exception). SECURITY INVARIANTS: channel-bound (no cross-connection replay), mutual, accepts a member it never paired (the mesh property)."
required_stages = ["impl", "unit"]  # ACTIVATED Mesh-D1 (codec slice: MK derivation + mutual channel-bound seed-proof transcript/verify + frame codec). D2 enriches impl (connect-time handshake + ConnEntry flag), D5 enriches impl (gate swap is_trusted→is_member at the five inbound sites). unit = MK derivation; valid/forged/wrong-subnet/wrong-epoch/cross-connection-replay/mutual-fail; D5 adds gate admits-unpaired-member / rejects-non-member.
[[requirements]]
id = "REQ-MESH-2"
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)."
required_stages = []  # activate at mesh execution. impl = roster store (union-merge + lease, atomic, corrupt-degrades-empty) + pairing-seed full-roster transfer (Frame::Seed gains the roster) + on-connect roster exchange/merge + tombstone propagation + admission suppression. unit = merge lease (strictly-greater self-entry) + union convergence + tombstone dominates/suppresses-reinsert/clears-on-repair + persist-through-silence + serde additive.
[[requirements]]
id = "REQ-MESH-3"
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."
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).
[[requirements]]
id = "REQ-MESH-4"
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."
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.
[[requirements]]
id = "REQ-MESH-5"
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."
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.
[[requirements]]
id = "REQ-MESH-6"
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.)"
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).


Everything's clear. The architecture itself decides the shape: connections aren't subnet-scoped, gates are per-`(subnet,origin)` per-item → **ConnEntry caches a proven-subnet SET**. This needs one wire extension (a `Hello` frame carrying the subnet-name list) on top of D1's `Challenge`/`Proof`. No user fork — the codebase determines it. Writing the plan.


Write

# Mesh-D2 — seed-proof at connect + ConnEntry membership set

> JIT plan for `SUBNET-MESH-PLAN.md` §Build phases · phase 2. **Wires the D1 codec onto live connections.** Run the mutual channel-bound seed-proof on every dial + accept in `nethost` *before* the connection is usable; cache the proven membership on `ConnEntry`; fail → drop. Verify/enable QUIC keep-alive so conns stay warm. **Old `is_trusted` gates keep running in parallel** (belt + suspenders — no regression; the gate swap is D5). **Enriches REQ-MESH-1 (impl)**; activates REQ-MESH-1 **int**.

## The one design decision (and why it's forced, not chosen)

D1 imagined one `subnet_id` per connection. The codebase says otherwise:

- `dial(addr)` (`broker.rs:593/605`) takes only an address — **connections are not subnet-scoped**. iroh dedups to one QUIC conn per peer-pair on the shared `SPT_NET_ALPN`, so a single conn carries traffic for **every subnet the two nodes share**.
- The five inbound gates D5 will swap are all **per-`(subnet, origin)`, evaluated per item** (`registryhost.rs:81/228/291`, `notifsync.rs:60/106`, `sync.rs:102/107`) — each registry row / notif / sync item carries its own `subnet`, and the gate asks `is_trusted(item.subnet, origin)`.

Therefore the cached verdict D5 consumes must answer *"has this peer proven subnet S?"* for **any** S an item names → **a set of proven subnets per `ConnEntry`, not a bool.** Single-subnet-per-conn would require threading a subnet through `dial`/accept (more plumbing) and still couldn't answer the multi-subnet gate. The set is both more correct and less invasive.

**Decision:** at connect, the two nodes prove the **full set of subnets they share**; `ConnEntry` caches `proven_subnets: HashSet<String>` (current-epoch). D5's gate becomes `conn.proven_subnets.contains(item.subnet)`.

## The connect-time exchange (one dedicated control stream, before the conn is usable)

The seed-proof rides the **first bidi stream**, opened by the dialer and consumed *before* `register_conn` wires the general `accept_bi` app pump — so it is never confused with an app stream.

```
dial:    connect → open_bi(ctrl) → run_seedproof(Dialer)   → ok? register_conn(conn, proven) : drop
accept:  connecting.await → conn → accept_bi(ctrl)         → ok? register_conn(conn, proven) : drop
         (pairing-ALPN branch is untouched — pre-trust, separate)
```

`run_seedproof` (new `spt-daemon` module `seedproofx.rs`, using `spt_net::net::mesh::seedproof` primitives — orchestration lives in the daemon; the crypto stays in `spt-net`):

1. **Dialer → `Hello { nonce_d, subnets: [names] }`** — its local subnet names + a fresh 32-byte OS-CSPRNG nonce.
2. **Acceptor → `Hello { nonce_a, subnets: shared }`** — replies with **only the intersection** of the dialer's list and its own (minimizes name disclosure to an unauthenticated dialer; see Security notes). `shared` is sorted (deterministic order both sides agree on).
3. Empty `shared` ⇒ both **drop** (a legit member always shares ≥1 subnet; no shared subnet = nothing to mesh).
4. For each `S` in `shared`: each side derives `MK_S = MembershipKey::derive(seed_S, S, epoch_S)` **from its own local subnet record** (each uses *its own* `epoch_S` — an epoch skew makes the tags diverge → exact-epoch reject, which is correct), builds the shared `SeedProofTranscript { S, epoch_S, dialer_pub, acceptor_pub, nonce_d, nonce_a }`, sends `Proof { tag_S(own_role) }`, and `verify`s the peer's `Proof` for the opposite role.
5. `proven = { S | peer's tag verified }`. **Any agreed-shared subnet that fails to prove ⇒ drop the whole connection** (an impostor naming a subnet it can't prove, or an epoch mismatch — neither is a usable member conn). On full success `proven == shared`; cache it.

Nonces are exchanged **once per connection** and reused across the per-subnet proofs (the transcript already binds `subnet_id` + `seed_epoch`, so per-subnet tags differ; the nonces only need per-connection freshness). Pubkeys come from the QUIC handshake (`conn.remote_id()` + local identity) — the channel binding.

### Wire: extend the D1 frame set (additive)

`SeedProofFrame` (D1: `Challenge{nonce}` / `Proof{tag}`) gains:

```rust
Hello { nonce: [u8; NONCE_LEN], subnets: Vec<String> },   // tag byte 2; len-prefixed count + len-prefixed names
```

`Challenge` is effectively subsumed by `Hello` (nonce + subnet list in one frame) — keep `Challenge` defined (D1 tests pin it) but the D2 driver uses `Hello`/`Proof`. `decode` stays total (unknown tag / bad length / bad UTF-8 ⇒ `None`). Length-prefix the name vector (u32 count, then per name u32 len + bytes) — same framing discipline as `pairing::wire`.

Subnet **names** go on the wire (not seeds, not epochs). Names are human labels, not secrets; the seed never leaves the node (ADR-0017 invariant). See Security notes.

## ConnEntry + register_conn changes (`nethost.rs`)

```rust
struct ConnEntry {
    conn: Connection,
    remote_id_hex: String,
    proven_subnets: HashSet<String>,   // NEW — current-epoch membership proven at connect
}
```

- `register_conn(shared, conn)` → `register_conn(shared, conn, proven_subnets)` — stores the set; everything else (presence append, closed-watcher, app `accept_bi` loop) unchanged. The app `accept_bi` loop now starts on the conn **after** the ctrl stream is consumed, so stream #1 (ctrl) is never registered as an app stream.
- New accessor `conn_proven_subnets(conn_id) -> HashSet<String>` (the seam D5 reads; unused by any gate at D2 — belt + suspenders).
- `dial` and the accept-loop trusted branch each run `run_seedproof` and drop on failure (don't register). Drop = simply return without `register_conn` (dialer) / let the conn fall out of scope (acceptor) → QUIC closes it.

### Where the subnet material comes from

`NetHost` has no `SubnetStore` access today. For D2, `run_seedproof` reads `SubnetStore::load()` at connect time (always-current across a future rotation; connects are rare, the load is cheap). No new long-lived handle, no snapshot-staleness. (D7's confidential re-seed push may want a live handle; out of scope here.) Pass the loaded `Vec<(name, seed_bytes, epoch)>` into the exchange. `nethost` already sits in `spt-daemon` and may depend on `spt-store`.

## Keep-alive (open item #1 — verify, then set)

Connections must stay warm so re-proof is **restart/partition/rotation-only**, never a per-message tax. `endpoint.rs` binds via `Endpoint::builder(presets::N0)` with **no explicit transport config**, so the conn lives under iroh/quinn defaults.

**Execution step (verify-then-set, don't guess):**
1. Determine iroh 0.98.2 `presets::N0` defaults for `max_idle_timeout` and `keep_alive_interval` (quinn's `keep_alive_interval` default is `None` = no keep-alive → conns idle out). Confirm the builder exposes a transport-config seam.
2. Set an explicit transport config with **`keep_alive_interval` < `max_idle_timeout`** (e.g. keep-alive ≈ idle/3) so an otherwise-idle member conn is held open by PING frames. Pick conservative values (idle on the order of tens of seconds; keep-alive a few seconds under it) — "multi-week" means *survives indefinite idleness while both ends are up*, not a single conn object literally pinned for weeks; a real restart/partition drops it and the next connect re-proves.
3. Apply on the production scope; the change is transport-only and orthogonal to `BindScope` (loopback hermetic tests inherit it harmlessly).

Tag the transport-config change `// [impl->REQ-MESH-1]` (keep-warm is part of the REQ-MESH-1 "kept warm via QUIC keep-alive" clause).

## Security notes (the invariants this phase must not break)

- **Channel-bound + mutual** — inherited from the D1 transcript (both handshake pubkeys + both nonces in the MAC; both sides prove). D2 must feed the *real* handshake pubkeys (`conn.remote_id()`, local identity) and *fresh* per-connect nonces — not fixed test arrays.
- **Drop-closed, not gate-open** — a failed/empty proof drops the connection; it does **not** fall through to an unauthenticated usable conn. Until D5 the `is_trusted` gates also still run, so even a (hypothetical) proven non-member conn carries no authority.
- **No-regression** — a legit directly-paired peer shares the subnet seed at the same epoch (epoch only bumps at D7's rotation; pre-D7 it's stable), so it always proves → never dropped. Note the one edge: a pre-existing epoch skew would now drop a conn `is_trusted` would have allowed; impossible before D7, acceptable to surface then.
- **Name disclosure** — the acceptor replies with the **intersection only**, so an unauthenticated dialer learns only which of the names *it already supplied* are shared (a weak name-enumeration oracle, not a seed/membership oracle). Acceptable under the same-user threat model (ADR-0017); the seed-proof, not name secrecy, is the boundary.

## Tests

**unit (`spt-net`, `mesh::seedproof`)** — extend D1's codec tests for the new frame, `// [unit->REQ-MESH-1]`:
- `Hello` round-trips (nonce + multi-name list incl. empty list and unicode name); malformed (bad count, short name, non-UTF-8, unknown tag) ⇒ `None`.

**int (`spt-daemon`, `nethost` tests)** — the connect-level integration, `// [int->REQ-MESH-1]`:
1. **both members → conn + proof** — two in-process `NetHost`s sharing a subnet (same seed+epoch) dial; assert the conn registers on both sides AND `conn_proven_subnets` contains the shared subnet on both.
2. **multi-subnet set** — two hosts sharing {`home`,`work`} but not `solo`; assert `proven_subnets == {home, work}` on both (the set, not a bool; `solo` absent).
3. **non-prover dropped** — a host with the wrong seed (or no shared subnet) dials a member; assert the connection does **not** register (dropped) on the acceptor and the dialer sees a dropped/closed conn. (Models "a non-prover is dropped.")
4. **keep-alive** *(light)* — assert the transport config carries a non-`None` `keep_alive_interval` strictly less than `max_idle_timeout` (config-level assertion; a real multi-week idle isn't testable in CI). If iroh hides these post-build, assert at the config-construction seam instead.

Hermetic throughout (`BindScope::Loopback`, `RelayPolicy::Disabled`, `LocalDiscovery::Off`) — same shape as the existing `two_hosts_dial_over_loopback`.

## Done when

- Seed-proof runs on every trusted-ALPN dial + accept before the conn is usable; `ConnEntry.proven_subnets` populated; failure/empty drops the conn.
- `cargo test -p spt-net mesh::seedproof` + `cargo test -p spt-daemon nethost` green; the four int cases pass.
- `cargo build -p spt-daemon` + `-p spt-net` (and `spt-net --no-default-features`) clean; mesh module stays `net`-gated.
- `keep_alive_interval` < `max_idle_timeout` confirmed set on the production endpoint.
- The existing `two_hosts_dial_over_loopback` still passes (now with a shared subnet in the hermetic config so the proof succeeds) — **no-regression** proven.
- `traceable-reqs check` clean with **REQ-MESH-1 `required_stages = ["impl","unit","int"]`** (D2 activates `int`; enriches `impl`). Evidence tags on the connect wiring, the `ConnEntry` set, the transport config, and the int tests.
- The `is_trusted` gates are **untouched** (D5 owns the swap) — grep confirms the five call sites unchanged.
- Commit: `feat(mesh): seed-proof at connect + ConnEntry membership set (REQ-MESH-1) — Mesh-D2`.

## Carry-forward (not D2)

- **Pre-D5 open item #3** — confirm the self-update path is node-local (ADR-0016), not riding member conns, before the cutover roll. Not D2.
- **Open item #2** (REQ-SUBNET-5 probe loop serial? → D8).
- D3 consumes nothing from D2's wire beyond the established conns; the roster (`Frame::Seed` growth) is independent.