45	/// Wire form is a flat lowercase tag (see [`EndpointType::as_tag`]). Shells
46	/// carry their concrete kind as `shell:<kind>` so the Shell family is
47	/// recognizable before any concrete Shell type is defined.
48	#[derive(Debug, Clone, PartialEq, Eq)]
49	pub enum EndpointType {
50	    /// Minimal participant: a perch + poll listener, no Psyche, no wrapper.
51	    /// Heir to the sister's "ready agent".
52	    ReadyAgent,
53	    /// A Self with a Psyche companion. Heir to the sister's "live agent".
54	    LiveAgent,
55	    /// The Psyche companion's own perch, distinct from its LiveAgent's.
56	    Psyche,
57	    /// A subagent's perch under a parent LiveAgent.
58	    Worker,
59	    /// A machine in an SPT subnet, identified by an Ed25519 public key.
60	    SptNode,
61	    /// Broker endpoint: presence resolution + shell brokering. Concept locked,
62	    /// impl deferred past v1 — but a first-class tag day-one.
63	    PresenceChannel,
64	    /// A driven surface controlled by a remote agent. `String` is the concrete
65	    /// Shell kind (e.g. `game_robot`); concrete kinds are deferred past v1.
66	    Shell(String),
67	    /// An endpoint type beyond the day-one set (open type system). Holds the
68	    /// raw tag verbatim so it round-trips unchanged.
69	    Other(String),
70	}
71
72	impl EndpointType {
73	    /// Prefix marking a Shell tag on the wire.
74	    const SHELL_PREFIX: &'static str = "shell:";
75
76	    /// The stable wire tag for this type. Inverse of [`EndpointType::from_tag`].
77	    // [impl->REQ-EP-1]
78	    pub fn as_tag(&self) -> String {
79	        match self {
80	            EndpointType::ReadyAgent => "ready_agent".to_string(),
81	            EndpointType::LiveAgent => "live_agent".to_string(),
82	            EndpointType::Psyche => "psyche".to_string(),
83	            EndpointType::Worker => "worker".to_string(),
84	            EndpointType::SptNode => "spt_node".to_string(),
85	            EndpointType::PresenceChannel => "presence_channel".to_string(),
86	            EndpointType::Shell(kind) => format!("{}{}", Self::SHELL_PREFIX, kind),
87	            EndpointType::Other(tag) => tag.clone(),
88	        }
89	    }
90
91	    /// Decode a wire tag. **Never fails** — an unrecognized tag becomes
92	    /// [`EndpointType::Other`] (open type system; forward-compat). A `shell:`
93	    /// prefix yields a [`EndpointType::Shell`] of the trailing kind.
94	    // [impl->REQ-EP-1]
95	    pub fn from_tag(tag: &str) -> EndpointType {
96	        match tag {
97	            "ready_agent" => EndpointType::ReadyAgent,
98	            "live_agent" => EndpointType::LiveAgent,
99	            "psyche" => EndpointType::Psyche,
100	            "worker" => EndpointType::Worker,
101	            "spt_node" => EndpointType::SptNode,
102	            "presence_channel" => EndpointType::PresenceChannel,
103	            other => {
104	                if let Some(kind) = other.strip_prefix(Self::SHELL_PREFIX) {
105	                    EndpointType::Shell(kind.to_string())
106	                } else {
107	                    EndpointType::Other(other.to_string())
108	                }
109	            }
110	        }
111	    }
112
113	    /// The structural family of this type.
114	    // [impl->REQ-EP-2]

/// Wire form is a flat lowercase tag (see [`EndpointType::as_tag`]). Shells
/// carry their concrete kind as `shell:<kind>` so the Shell family is
/// recognizable before any concrete Shell type is defined.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EndpointType {
    /// Minimal participant: a perch + poll listener, no Psyche, no wrapper.
    /// Heir to the sister's "ready agent".
    ReadyAgent,
    /// A Self with a Psyche companion. Heir to the sister's "live agent".
    LiveAgent,
    /// The Psyche companion's own perch, distinct from its LiveAgent's.
    Psyche,
    /// A subagent's perch under a parent LiveAgent.
    Worker,
    /// A machine in an SPT subnet, identified by an Ed25519 public key.
    SptNode,
    /// Broker endpoint: presence resolution + shell brokering. Concept locked,
    /// impl deferred past v1 — but a first-class tag day-one.
    PresenceChannel,
    /// A driven surface controlled by a remote agent. `String` is the concrete
    /// Shell kind (e.g. `game_robot`); concrete kinds are deferred past v1.
    Shell(String),
    /// An endpoint type beyond the day-one set (open type system). Holds the
    /// raw tag verbatim so it round-trips unchanged.
    Other(String),
}

impl EndpointType {
    /// Prefix marking a Shell tag on the wire.
    const SHELL_PREFIX: &'static str = "shell:";

    /// The stable wire tag for this type. Inverse of [`EndpointType::from_tag`].
    // [impl->REQ-EP-1]
    pub fn as_tag(&self) -> String {
        match self {
            EndpointType::ReadyAgent => "ready_agent".to_string(),
            EndpointType::LiveAgent => "live_agent".to_string(),
            EndpointType::Psyche => "psyche".to_string(),
            EndpointType::Worker => "worker".to_string(),
            EndpointType::SptNode => "spt_node".to_string(),
            EndpointType::PresenceChannel => "presence_channel".to_string(),
            EndpointType::Shell(kind) => format!("{}{}", Self::SHELL_PREFIX, kind),
            EndpointType::Other(tag) => tag.clone(),
        }
    }

    /// Decode a wire tag. **Never fails** — an unrecognized tag becomes
    /// [`EndpointType::Other`] (open type system; forward-compat). A `shell:`
    /// prefix yields a [`EndpointType::Shell`] of the trailing kind.
    // [impl->REQ-EP-1]
    pub fn from_tag(tag: &str) -> EndpointType {
        match tag {
            "ready_agent" => EndpointType::ReadyAgent,
            "live_agent" => EndpointType::LiveAgent,
            "psyche" => EndpointType::Psyche,
            "worker" => EndpointType::Worker,
            "spt_node" => EndpointType::SptNode,
            "presence_channel" => EndpointType::PresenceChannel,
            other => {
                if let Some(kind) = other.strip_prefix(Self::SHELL_PREFIX) {
                    EndpointType::Shell(kind.to_string())
                } else {
                    EndpointType::Other(other.to_string())
                }
            }
        }
    }

    /// The structural family of this type.
    // [impl->REQ-EP-2]


`from_tag("gateway")` → `Other("gateway")` → gateway. Good. Edit wan.rs: imports + helper + signature.


Edit

use spt_msg::deliver::{deliver_tcp, perch_exists};
use spt_net::net::wanmsg::WanMessage;
use spt_proto::event::{compose_msg_event, parse_event, EVENT_TYPE_USER_MSG};
use spt_store::spool;

use crate::access::{access_check, classify_inbound, AccessDecision};

use spt_msg::deliver::{deliver_tcp, perch_exists};
use spt_net::net::registry::Instance;
use spt_net::net::wanmsg::WanMessage;
use spt_proto::endpoint::EndpointType;
use spt_proto::event::{compose_msg_event, is_gateway_endpoint, parse_event, EVENT_TYPE_USER_MSG};
use spt_store::spool;

use crate::access::{access_check, classify_inbound, AccessDecision};
use crate::registryhost::RegistryHost;


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

use spt_msg::deliver::{deliver_tcp, perch_exists};
use spt_net::net::registry::Instance;
use spt_net::net::wanmsg::WanMessage;
use spt_proto::endpoint::EndpointType;
use spt_proto::event::{compose_msg_event, is_gateway_endpoint, parse_event, EVENT_TYPE_USER_MSG};
use spt_store::spool;

use crate::access::{access_check, classify_inbound, AccessDecision};
use crate::registryhost::RegistryHost;

//! WAN message receive funnel (M4-D5a) — wire record → access gate → op-id
//! dedup → the existing local delivery path.
//!
//! The sending node resolved the target (D3c policy), dialed, and wrote
//! [`WanMessage`] NDJSON lines onto a broker-owned QUIC stream (D4). This
//! module is the receiving node's funnel from a decoded record to the local
//! REQ-MSG machinery — it adds exactly two things the local path doesn't have,
//! then delegates:
//!
//! 1. **the access gate** ([`crate::access`], ADR-0009): every wire-inbound
//!    record passes [`access_check`] with the **handshake-proven** origin node
//!    (the broker stream's `remote_id_hex` — never a payload field,
//!    REQ-HAZARD-WAN-ORIGIN-AUTH);
//! 2. **op-id idempotency** (REQ-HAZARD-RESTART-IDEMPOTENT): the wire feed
//!    replays — a sender's journaled redrive, or this brain restarting and
//!    resubscribing from an earlier cursor — so delivery dedups on the
//!    sender-minted durable `op_id`.
//!
//! ## Exactly-once boundaries (honest accounting)
//!
//! - **Spool path (offline target):** fully exactly-once — the op-id claim and
//!   the spool row commit in ONE SQLite transaction
//!   ([`spool::spool_wan_message_at`]); a crash on either side of the write
//!   replays to the same single row.
//! - **Live-TCP path:** check → TCP write → claim. A crash in the gap between
//!   a completed TCP write and the claim re-delivers that op on replay (an
//!   at-least-once window bounded to one in-flight record; the live listener
//!   path has no durable receipt to dedup against). Claim-before-write was
//!   rejected because a crash in ITS gap silently drops the message — for a
//!   messaging substrate, a rare duplicate beats a rare silent drop. Tracked
//!   in `docs/FAULT-MATRIX.md`.

use std::borrow::Cow;
use std::path::Path;

use spt_msg::deliver::{deliver_tcp, perch_exists};
use spt_net::net::wanmsg::WanMessage;
use spt_proto::event::{compose_msg_event, parse_event, EVENT_TYPE_USER_MSG};
use spt_store::spool;

use crate::access::{access_check, classify_inbound, AccessDecision};

/// Re-stamp a WAN-inbound `user-msg` body down to plain `msg` **unless** the
/// transport-proven origin is a user-backed endpoint (CONTEXT §Gateway; the
/// KH 7.5 cross-node enforcement — authority is the proven origin, never the
/// payload). The honored `user-msg` form is a verbatim typed-envelope body, so
/// a forged one would otherwise reach the receiver as user-authority; here we
/// decode it and recompose a plain `msg` envelope, preserving `from` + the inner
/// body. A non-`user-msg` body (a plain message, any other typed envelope, or a
/// bare body) is returned unchanged.
///
/// `origin_user_backed` is resolved by the caller from the proven origin node:
/// over WAN, the ONLY provably user-backed origin is a registry-resolved Gateway
/// endpoint (delivered in T5). Until then callers pass `false`, so every WAN
/// `user-msg` is re-stamped — fail-closed by construction.
// [impl->REQ-MSG-5]
// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
fn restamp_wan_user_msg(body: &str, origin_user_backed: bool) -> Cow<'_, str> {
    if origin_user_backed {
        return Cow::Borrowed(body);
    }
    match parse_event(body) {
        Some(ev) if ev.event_type.as_deref() == Some(EVENT_TYPE_USER_MSG) => {
            Cow::Owned(compose_msg_event(ev.from().unwrap_or(""), &ev.body))
        }
        _ => Cow::Borrowed(body),
    }
}

/// Wall-clock now (epoch ms) — the reply-window clock.
fn now_ms() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0)
}

/// What the funnel did with one inbound record.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WanOutcome {
    /// Delivered live over TCP to the target's listener (op claimed after).
    DeliveredTcp,
    /// Spooled for later drain, atomically with the op claim.
    Spooled,
    /// The op_id was already claimed — a replay; nothing written.
    Duplicate,
    /// Refused at the access gate (ADR-0009) — dropped before any delivery.
    Refused,
    /// No perch for the target on this node — dropped (not claimed: if the
    /// perch appears later, a replayed record may still land).
    NoPerch,
}

/// Funnel one decoded wire record into local delivery. `origin_node` MUST be
/// the QUIC handshake-proven remote node id (the broker's
/// `NetStreamInfo::remote_id_hex` for the stream the record arrived on) —
/// never anything read out of record bytes (REQ-HAZARD-WAN-ORIGIN-AUTH).
// [impl->REQ-NET-1]
// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
// [impl->REQ-HAZARD-RESTART-IDEMPOTENT]
pub fn receive_wan(msg: &WanMessage, origin_node: &str, owlery: &Path) -> WanOutcome {
    // Gate 2 of ADR-0009 (visibility ran at the sender's resolution; grants
    // gate capabilities, not messages). Messages classify reply-vs-unsolicited
    // against the target's own recent outbound (the stateful firewall): a
    // correlated reply passes a whitelist that would refuse the same node
    // unsolicited.
    let class = classify_inbound(&msg.target, origin_node, now_ms());
    if access_check(&msg.target, origin_node, class) == AccessDecision::Deny {
        return WanOutcome::Refused;
    }

    if !perch_exists(&msg.target) {
        return WanOutcome::NoPerch;
    }

    let perch_path =
        spt_store::perch::resolve_perch_path(&msg.target, spt_store::perch::ParentHint::Infer);

    // Replay check first: a claimed op is done regardless of which path
    // claimed it.
    match spool::wan_seen_at(&perch_path, &msg.op_id) {
        Ok(true) => return WanOutcome::Duplicate,
        Ok(false) => {}
        Err(_) => return WanOutcome::NoPerch,
    }

    // user-msg identity gate, WAN-ingress half (REQ-MSG-5; KH 7.5): re-stamp a
    // forged/unverified user-msg down to plain msg before it reaches the target.
    // T4 fail-closes (no WAN origin is yet provably user-backed); T5 resolves a
    // Gateway origin from the registry and threads it here.
    let delivered_body = restamp_wan_user_msg(&msg.body, false);

    // Live-TCP first (mirrors the local TCP-xor-spool discipline, 4.5), claim
    // after the completed write (see module docs for the window accounting).
    if deliver_tcp(&msg.target, &msg.from, &delivered_body, owlery) {
        let _ = spool::wan_mark_seen_at(&perch_path, &msg.op_id);
        return WanOutcome::DeliveredTcp;
    }

    // Offline: claim + spool in one transaction — exactly-once.
    match spool::spool_wan_message_at(&perch_path, &msg.from, &delivered_body, &msg.op_id) {
        Ok(true) => WanOutcome::Spooled,
        Ok(false) => WanOutcome::Duplicate,
        Err(_) => WanOutcome::NoPerch,
    }
}

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

    // [unit->REQ-MSG-5] WAN-ingress re-stamp (KH 7.5): a user-msg body from an
    // origin not provably user-backed (the T4 fail-closed default) is
    // re-stamped down to plain msg, preserving from + the decoded inner body; a
    // user-backed origin (the T5 Gateway path) keeps it verbatim; a plain msg
    // envelope and a bare body are untouched either way.
    #[test]
    fn wan_user_msg_restamped_unless_origin_user_backed() {
        let um = compose_user_msg_event("playdate-gw", "ship it\nnow & later");

        // Not user-backed → re-stamped to msg.
        let out = restamp_wan_user_msg(&um, false).into_owned();
        let p = parse_event(&out).unwrap();
        assert_eq!(p.event_type.as_deref(), Some("msg"));
        assert_eq!(p.from(), Some("playdate-gw"));
        assert_eq!(p.body, "ship it\nnow & later");

        // User-backed origin → kept verbatim (the T5 positive path).
        assert_eq!(restamp_wan_user_msg(&um, true).into_owned(), um);

        // A plain msg envelope is untouched.
        let plain = compose_msg_event("doyle", "hi");
        assert_eq!(restamp_wan_user_msg(&plain, false).into_owned(), plain);

        // A bare (non-envelope) body is untouched.
        assert_eq!(restamp_wan_user_msg("hello", false).into_owned(), "hello");
    }
}

-use spt_proto::event::{compose_msg_event, parse_event, EVENT_TYPE_USER_MSG};

+use spt_proto::event::{compose_msg_event, is_gateway_endpoint, parse_event, EVENT_TYPE_USER_MSG};


/// Wall-clock now (epoch ms) — the reply-window clock.
fn now_ms() -> u64 {

/// Resolve whether a WAN-inbound `user-msg`'s origin is provably **user-backed**
/// (REQ-MSG-6 — CONTEXT §Gateway): among the origin endpoint's advertised
/// instances, the one hosted on the **QUIC-handshake-proven** `origin_node`
/// advertises the Gateway type ([`spt_proto::event::GATEWAY_TAG`]). Keyed on the
/// proven node, NEVER the wire `from` — a record whose `from` claims a gateway
/// id is inert unless the proven node actually hosts that gateway-typed
/// endpoint. An absent or non-gateway advertised type → `false`: the fail-closed
/// re-stamp (an N-1 node that does not yet advertise `endpoint_type` degrades
/// gracefully to plain `msg`).
///
/// This resolves the *advertised* type; it is **not** a forgery defense. The
/// trust boundary is subnet membership (operator-ratified 2026-06-13): a subnet
/// member is already trusted, so a member forging the Gateway type is out of
/// scope by construction. The proven-node keying is registry-addressing
/// correctness, not an extra trust layer.
// [impl->REQ-MSG-6]
fn origin_user_backed(instances: &[Instance], origin_node: &str) -> bool {
    instances.iter().any(|i| {
        i.node == origin_node
            && i.endpoint_type
                .as_deref()
                .is_some_and(|tag| is_gateway_endpoint(&EndpointType::from_tag(tag)))
    })
}

/// Wall-clock now (epoch ms) — the reply-window clock.
fn now_ms() -> u64 {


/// Resolve whether a WAN-inbound `user-msg`'s origin is provably **user-backed**
/// (REQ-MSG-6 — CONTEXT §Gateway): among the origin endpoint's advertised
/// instances, the one hosted on the **QUIC-handshake-proven** `origin_node`
/// advertises the Gateway type ([`spt_proto::event::GATEWAY_TAG`]). Keyed on the
/// proven node, NEVER the wire `from` — a record whose `from` claims a gateway
/// id is inert unless the proven node actually hosts that gateway-typed
/// endpoint. An absent or non-gateway advertised type → `false`: the fail-closed
/// re-stamp (an N-1 node that does not yet advertise `endpoint_type` degrades
/// gracefully to plain `msg`).
///
/// This resolves the *advertised* type; it is **not** a forgery defense. The
/// trust boundary is subnet membership (operator-ratified 2026-06-13): a subnet
/// member is already trusted, so a member forging the Gateway type is out of
/// scope by construction. The proven-node keying is registry-addressing
/// correctness, not an extra trust layer.
// [impl->REQ-MSG-6]
fn origin_user_backed(instances: &[Instance], origin_node: &str) -> bool {
    instances.iter().any(|i| {
        i.node == origin_node
            && i.endpoint_type
                .as_deref()
                .is_some_and(|tag| is_gateway_endpoint(&EndpointType::from_tag(tag)))
    })
}

/// Wall-clock now (epoch ms) — the reply-window clock.
fn now_ms() -> u64 {

//! WAN message receive funnel (M4-D5a) — wire record → access gate → op-id
//! dedup → the existing local delivery path.
//!
//! The sending node resolved the target (D3c policy), dialed, and wrote
//! [`WanMessage`] NDJSON lines onto a broker-owned QUIC stream (D4). This
//! module is the receiving node's funnel from a decoded record to the local
//! REQ-MSG machinery — it adds exactly two things the local path doesn't have,
//! then delegates:
//!
//! 1. **the access gate** ([`crate::access`], ADR-0009): every wire-inbound
//!    record passes [`access_check`] with the **handshake-proven** origin node
//!    (the broker stream's `remote_id_hex` — never a payload field,
//!    REQ-HAZARD-WAN-ORIGIN-AUTH);
//! 2. **op-id idempotency** (REQ-HAZARD-RESTART-IDEMPOTENT): the wire feed
//!    replays — a sender's journaled redrive, or this brain restarting and
//!    resubscribing from an earlier cursor — so delivery dedups on the
//!    sender-minted durable `op_id`.
//!
//! ## Exactly-once boundaries (honest accounting)
//!
//! - **Spool path (offline target):** fully exactly-once — the op-id claim and
//!   the spool row commit in ONE SQLite transaction
//!   ([`spool::spool_wan_message_at`]); a crash on either side of the write
//!   replays to the same single row.
//! - **Live-TCP path:** check → TCP write → claim. A crash in the gap between
//!   a completed TCP write and the claim re-delivers that op on replay (an
//!   at-least-once window bounded to one in-flight record; the live listener
//!   path has no durable receipt to dedup against). Claim-before-write was
//!   rejected because a crash in ITS gap silently drops the message — for a
//!   messaging substrate, a rare duplicate beats a rare silent drop. Tracked
//!   in `docs/FAULT-MATRIX.md`.

use std::borrow::Cow;
use std::path::Path;

use spt_msg::deliver::{deliver_tcp, perch_exists};
use spt_net::net::registry::Instance;
use spt_net::net::wanmsg::WanMessage;
use spt_proto::endpoint::EndpointType;
use spt_proto::event::{compose_msg_event, is_gateway_endpoint, parse_event, EVENT_TYPE_USER_MSG};
use spt_store::spool;

use crate::access::{access_check, classify_inbound, AccessDecision};
use crate::registryhost::RegistryHost;

/// Re-stamp a WAN-inbound `user-msg` body down to plain `msg` **unless** the
/// transport-proven origin is a user-backed endpoint (CONTEXT §Gateway; the
/// KH 7.5 cross-node enforcement — authority is the proven origin, never the
/// payload). The honored `user-msg` form is a verbatim typed-envelope body, so
/// a forged one would otherwise reach the receiver as user-authority; here we
/// decode it and recompose a plain `msg` envelope, preserving `from` + the inner
/// body. A non-`user-msg` body (a plain message, any other typed envelope, or a
/// bare body) is returned unchanged.
///
/// `origin_user_backed` is resolved by the caller from the proven origin node:
/// over WAN, the ONLY provably user-backed origin is a registry-resolved Gateway
/// endpoint (delivered in T5). Until then callers pass `false`, so every WAN
/// `user-msg` is re-stamped — fail-closed by construction.
// [impl->REQ-MSG-5]
// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
fn restamp_wan_user_msg(body: &str, origin_user_backed: bool) -> Cow<'_, str> {
    if origin_user_backed {
        return Cow::Borrowed(body);
    }
    match parse_event(body) {
        Some(ev) if ev.event_type.as_deref() == Some(EVENT_TYPE_USER_MSG) => {
            Cow::Owned(compose_msg_event(ev.from().unwrap_or(""), &ev.body))
        }
        _ => Cow::Borrowed(body),
    }
}

/// Wall-clock now (epoch ms) — the reply-window clock.
fn now_ms() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0)
}

/// What the funnel did with one inbound record.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WanOutcome {
    /// Delivered live over TCP to the target's listener (op claimed after).
    DeliveredTcp,
    /// Spooled for later drain, atomically with the op claim.
    Spooled,
    /// The op_id was already claimed — a replay; nothing written.
    Duplicate,
    /// Refused at the access gate (ADR-0009) — dropped before any delivery.
    Refused,
    /// No perch for the target on this node — dropped (not claimed: if the
    /// perch appears later, a replayed record may still land).
    NoPerch,
}

/// Funnel one decoded wire record into local delivery. `origin_node` MUST be
/// the QUIC handshake-proven remote node id (the broker's
/// `NetStreamInfo::remote_id_hex` for the stream the record arrived on) —
/// never anything read out of record bytes (REQ-HAZARD-WAN-ORIGIN-AUTH).
// [impl->REQ-NET-1]
// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
// [impl->REQ-HAZARD-RESTART-IDEMPOTENT]
pub fn receive_wan(msg: &WanMessage, origin_node: &str, owlery: &Path) -> WanOutcome {
    // Gate 2 of ADR-0009 (visibility ran at the sender's resolution; grants
    // gate capabilities, not messages). Messages classify reply-vs-unsolicited
    // against the target's own recent outbound (the stateful firewall): a
    // correlated reply passes a whitelist that would refuse the same node
    // unsolicited.
    let class = classify_inbound(&msg.target, origin_node, now_ms());
    if access_check(&msg.target, origin_node, class) == AccessDecision::Deny {
        return WanOutcome::Refused;
    }

    if !perch_exists(&msg.target) {
        return WanOutcome::NoPerch;
    }

    let perch_path =
        spt_store::perch::resolve_perch_path(&msg.target, spt_store::perch::ParentHint::Infer);

    // Replay check first: a claimed op is done regardless of which path
    // claimed it.
    match spool::wan_seen_at(&perch_path, &msg.op_id) {
        Ok(true) => return WanOutcome::Duplicate,
        Ok(false) => {}
        Err(_) => return WanOutcome::NoPerch,
    }

    // user-msg identity gate, WAN-ingress half (REQ-MSG-5; KH 7.5): re-stamp a
    // forged/unverified user-msg down to plain msg before it reaches the target.
    // T4 fail-closes (no WAN origin is yet provably user-backed); T5 resolves a
    // Gateway origin from the registry and threads it here.
    let delivered_body = restamp_wan_user_msg(&msg.body, false);

    // Live-TCP first (mirrors the local TCP-xor-spool discipline, 4.5), claim
    // after the completed write (see module docs for the window accounting).
    if deliver_tcp(&msg.target, &msg.from, &delivered_body, owlery) {
        let _ = spool::wan_mark_seen_at(&perch_path, &msg.op_id);
        return WanOutcome::DeliveredTcp;
    }

    // Offline: claim + spool in one transaction — exactly-once.
    match spool::spool_wan_message_at(&perch_path, &msg.from, &delivered_body, &msg.op_id) {
        Ok(true) => WanOutcome::Spooled,
        Ok(false) => WanOutcome::Duplicate,
        Err(_) => WanOutcome::NoPerch,
    }
}

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

    // [unit->REQ-MSG-5] WAN-ingress re-stamp (KH 7.5): a user-msg body from an
    // origin not provably user-backed (the T4 fail-closed default) is
    // re-stamped down to plain msg, preserving from + the decoded inner body; a
    // user-backed origin (the T5 Gateway path) keeps it verbatim; a plain msg
    // envelope and a bare body are untouched either way.
    #[test]
    fn wan_user_msg_restamped_unless_origin_user_backed() {
        let um = compose_user_msg_event("playdate-gw", "ship it\nnow & later");

        // Not user-backed → re-stamped to msg.
        let out = restamp_wan_user_msg(&um, false).into_owned();
        let p = parse_event(&out).unwrap();
        assert_eq!(p.event_type.as_deref(), Some("msg"));
        assert_eq!(p.from(), Some("playdate-gw"));
        assert_eq!(p.body, "ship it\nnow & later");

        // User-backed origin → kept verbatim (the T5 positive path).
        assert_eq!(restamp_wan_user_msg(&um, true).into_owned(), um);

        // A plain msg envelope is untouched.
        let plain = compose_msg_event("doyle", "hi");
        assert_eq!(restamp_wan_user_msg(&plain, false).into_owned(), plain);

        // A bare (non-envelope) body is untouched.
        assert_eq!(restamp_wan_user_msg("hello", false).into_owned(), "hello");
    }
}

+/// Resolve whether a WAN-inbound `user-msg`'s origin is provably **user-backed**

+/// (REQ-MSG-6 — CONTEXT §Gateway): among the origin endpoint's advertised

+/// instances, the one hosted on the **QUIC-handshake-proven** `origin_node`

+/// advertises the Gateway type ([`spt_proto::event::GATEWAY_TAG`]). Keyed on the

+/// proven node, NEVER the wire `from` — a record whose `from` claims a gateway

+/// id is inert unless the proven node actually hosts that gateway-typed

+/// endpoint. An absent or non-gateway advertised type → `false`: the fail-closed

+/// re-stamp (an N-1 node that does not yet advertise `endpoint_type` degrades

+/// This resolves the *advertised* type; it is **not** a forgery defense. The

+/// trust boundary is subnet membership (operator-ratified 2026-06-13): a subnet

+/// member is already trusted, so a member forging the Gateway type is out of

+/// scope by construction. The proven-node keying is registry-addressing

+/// correctness, not an extra trust layer.

+fn origin_user_backed(instances: &[Instance], origin_node: &str) -> bool {

+                .is_some_and(|tag| is_gateway_endpoint(&EndpointType::from_tag(tag)))


Now update `receive_wan` signature + the resolve call site.


/// Funnel one decoded wire record into local delivery. `origin_node` MUST be
/// the QUIC handshake-proven remote node id (the broker's
/// `NetStreamInfo::remote_id_hex` for the stream the record arrived on) —
/// never anything read out of record bytes (REQ-HAZARD-WAN-ORIGIN-AUTH).
// [impl->REQ-NET-1]
// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
// [impl->REQ-HAZARD-RESTART-IDEMPOTENT]
pub fn receive_wan(msg: &WanMessage, origin_node: &str, owlery: &Path) -> WanOutcome {

/// Funnel one decoded wire record into local delivery. `origin_node` MUST be
/// the QUIC handshake-proven remote node id (the broker's
/// `NetStreamInfo::remote_id_hex` for the stream the record arrived on) —
/// never anything read out of record bytes (REQ-HAZARD-WAN-ORIGIN-AUTH).
/// `registry` is the node's [`RegistryHost`]: the user-msg identity gate
/// resolves the origin endpoint's advertised type from it, keyed by
/// `origin_node` (REQ-MSG-6).
// [impl->REQ-NET-1]
// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
// [impl->REQ-HAZARD-RESTART-IDEMPOTENT]
pub fn receive_wan(
    msg: &WanMessage,
    origin_node: &str,
    owlery: &Path,
    registry: &RegistryHost,
) -> WanOutcome {


/// Funnel one decoded wire record into local delivery. `origin_node` MUST be
/// the QUIC handshake-proven remote node id (the broker's
/// `NetStreamInfo::remote_id_hex` for the stream the record arrived on) —
/// never anything read out of record bytes (REQ-HAZARD-WAN-ORIGIN-AUTH).
/// `registry` is the node's [`RegistryHost`]: the user-msg identity gate
/// resolves the origin endpoint's advertised type from it, keyed by
/// `origin_node` (REQ-MSG-6).
// [impl->REQ-NET-1]
// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
// [impl->REQ-HAZARD-RESTART-IDEMPOTENT]
pub fn receive_wan(
    msg: &WanMessage,
    origin_node: &str,
    owlery: &Path,
    registry: &RegistryHost,
) -> WanOutcome {

//! WAN message receive funnel (M4-D5a) — wire record → access gate → op-id
//! dedup → the existing local delivery path.
//!
//! The sending node resolved the target (D3c policy), dialed, and wrote
//! [`WanMessage`] NDJSON lines onto a broker-owned QUIC stream (D4). This
//! module is the receiving node's funnel from a decoded record to the local
//! REQ-MSG machinery — it adds exactly two things the local path doesn't have,
//! then delegates:
//!
//! 1. **the access gate** ([`crate::access`], ADR-0009): every wire-inbound
//!    record passes [`access_check`] with the **handshake-proven** origin node
//!    (the broker stream's `remote_id_hex` — never a payload field,
//!    REQ-HAZARD-WAN-ORIGIN-AUTH);
//! 2. **op-id idempotency** (REQ-HAZARD-RESTART-IDEMPOTENT): the wire feed
//!    replays — a sender's journaled redrive, or this brain restarting and
//!    resubscribing from an earlier cursor — so delivery dedups on the
//!    sender-minted durable `op_id`.
//!
//! ## Exactly-once boundaries (honest accounting)
//!
//! - **Spool path (offline target):** fully exactly-once — the op-id claim and
//!   the spool row commit in ONE SQLite transaction
//!   ([`spool::spool_wan_message_at`]); a crash on either side of the write
//!   replays to the same single row.
//! - **Live-TCP path:** check → TCP write → claim. A crash in the gap between
//!   a completed TCP write and the claim re-delivers that op on replay (an
//!   at-least-once window bounded to one in-flight record; the live listener
//!   path has no durable receipt to dedup against). Claim-before-write was
//!   rejected because a crash in ITS gap silently drops the message — for a
//!   messaging substrate, a rare duplicate beats a rare silent drop. Tracked
//!   in `docs/FAULT-MATRIX.md`.

use std::borrow::Cow;
use std::path::Path;

use spt_msg::deliver::{deliver_tcp, perch_exists};
use spt_net::net::registry::Instance;
use spt_net::net::wanmsg::WanMessage;
use spt_proto::endpoint::EndpointType;
use spt_proto::event::{compose_msg_event, is_gateway_endpoint, parse_event, EVENT_TYPE_USER_MSG};
use spt_store::spool;

use crate::access::{access_check, classify_inbound, AccessDecision};
use crate::registryhost::RegistryHost;

/// Re-stamp a WAN-inbound `user-msg` body down to plain `msg` **unless** the
/// transport-proven origin is a user-backed endpoint (CONTEXT §Gateway; the
/// KH 7.5 cross-node enforcement — authority is the proven origin, never the
/// payload). The honored `user-msg` form is a verbatim typed-envelope body, so
/// a forged one would otherwise reach the receiver as user-authority; here we
/// decode it and recompose a plain `msg` envelope, preserving `from` + the inner
/// body. A non-`user-msg` body (a plain message, any other typed envelope, or a
/// bare body) is returned unchanged.
///
/// `origin_user_backed` is resolved by the caller from the proven origin node:
/// over WAN, the ONLY provably user-backed origin is a registry-resolved Gateway
/// endpoint (delivered in T5). Until then callers pass `false`, so every WAN
/// `user-msg` is re-stamped — fail-closed by construction.
// [impl->REQ-MSG-5]
// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
fn restamp_wan_user_msg(body: &str, origin_user_backed: bool) -> Cow<'_, str> {
    if origin_user_backed {
        return Cow::Borrowed(body);
    }
    match parse_event(body) {
        Some(ev) if ev.event_type.as_deref() == Some(EVENT_TYPE_USER_MSG) => {
            Cow::Owned(compose_msg_event(ev.from().unwrap_or(""), &ev.body))
        }
        _ => Cow::Borrowed(body),
    }
}

/// Resolve whether a WAN-inbound `user-msg`'s origin is provably **user-backed**
/// (REQ-MSG-6 — CONTEXT §Gateway): among the origin endpoint's advertised
/// instances, the one hosted on the **QUIC-handshake-proven** `origin_node`
/// advertises the Gateway type ([`spt_proto::event::GATEWAY_TAG`]). Keyed on the
/// proven node, NEVER the wire `from` — a record whose `from` claims a gateway
/// id is inert unless the proven node actually hosts that gateway-typed
/// endpoint. An absent or non-gateway advertised type → `false`: the fail-closed
/// re-stamp (an N-1 node that does not yet advertise `endpoint_type` degrades
/// gracefully to plain `msg`).
///
/// This resolves the *advertised* type; it is **not** a forgery defense. The
/// trust boundary is subnet membership (operator-ratified 2026-06-13): a subnet
/// member is already trusted, so a member forging the Gateway type is out of
/// scope by construction. The proven-node keying is registry-addressing
/// correctness, not an extra trust layer.
// [impl->REQ-MSG-6]
fn origin_user_backed(instances: &[Instance], origin_node: &str) -> bool {
    instances.iter().any(|i| {
        i.node == origin_node
            && i.endpoint_type
                .as_deref()
                .is_some_and(|tag| is_gateway_endpoint(&EndpointType::from_tag(tag)))
    })
}

/// Wall-clock now (epoch ms) — the reply-window clock.
fn now_ms() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0)
}

/// What the funnel did with one inbound record.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WanOutcome {
    /// Delivered live over TCP to the target's listener (op claimed after).
    DeliveredTcp,
    /// Spooled for later drain, atomically with the op claim.
    Spooled,
    /// The op_id was already claimed — a replay; nothing written.
    Duplicate,
    /// Refused at the access gate (ADR-0009) — dropped before any delivery.
    Refused,
    /// No perch for the target on this node — dropped (not claimed: if the
    /// perch appears later, a replayed record may still land).
    NoPerch,
}

/// Funnel one decoded wire record into local delivery. `origin_node` MUST be
/// the QUIC handshake-proven remote node id (the broker's
/// `NetStreamInfo::remote_id_hex` for the stream the record arrived on) —
/// never anything read out of record bytes (REQ-HAZARD-WAN-ORIGIN-AUTH).
// [impl->REQ-NET-1]
// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
// [impl->REQ-HAZARD-RESTART-IDEMPOTENT]
pub fn receive_wan(msg: &WanMessage, origin_node: &str, owlery: &Path) -> WanOutcome {
    // Gate 2 of ADR-0009 (visibility ran at the sender's resolution; grants
    // gate capabilities, not messages). Messages classify reply-vs-unsolicited
    // against the target's own recent outbound (the stateful firewall): a
    // correlated reply passes a whitelist that would refuse the same node
    // unsolicited.
    let class = classify_inbound(&msg.target, origin_node, now_ms());
    if access_check(&msg.target, origin_node, class) == AccessDecision::Deny {
        return WanOutcome::Refused;
    }

    if !perch_exists(&msg.target) {
        return WanOutcome::NoPerch;
    }

    let perch_path =
        spt_store::perch::resolve_perch_path(&msg.target, spt_store::perch::ParentHint::Infer);

    // Replay check first: a claimed op is done regardless of which path
    // claimed it.
    match spool::wan_seen_at(&perch_path, &msg.op_id) {
        Ok(true) => return WanOutcome::Duplicate,
        Ok(false) => {}
        Err(_) => return WanOutcome::NoPerch,
    }

    // user-msg identity gate, WAN-ingress half (REQ-MSG-5; KH 7.5): re-stamp a
    // forged/unverified user-msg down to plain msg before it reaches the target.
    // T4 fail-closes (no WAN origin is yet provably user-backed); T5 resolves a
    // Gateway origin from the registry and threads it here.
    let delivered_body = restamp_wan_user_msg(&msg.body, false);

    // Live-TCP first (mirrors the local TCP-xor-spool discipline, 4.5), claim
    // after the completed write (see module docs for the window accounting).
    if deliver_tcp(&msg.target, &msg.from, &delivered_body, owlery) {
        let _ = spool::wan_mark_seen_at(&perch_path, &msg.op_id);
        return WanOutcome::DeliveredTcp;
    }

    // Offline: claim + spool in one transaction — exactly-once.
    match spool::spool_wan_message_at(&perch_path, &msg.from, &delivered_body, &msg.op_id) {
        Ok(true) => WanOutcome::Spooled,
        Ok(false) => WanOutcome::Duplicate,
        Err(_) => WanOutcome::NoPerch,
    }
}

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

    // [unit->REQ-MSG-5] WAN-ingress re-stamp (KH 7.5): a user-msg body from an
    // origin not provably user-backed (the T4 fail-closed default) is
    // re-stamped down to plain msg, preserving from + the decoded inner body; a
    // user-backed origin (the T5 Gateway path) keeps it verbatim; a plain msg
    // envelope and a bare body are untouched either way.
    #[test]
    fn wan_user_msg_restamped_unless_origin_user_backed() {
        let um = compose_user_msg_event("playdate-gw", "ship it\nnow & later");

        // Not user-backed → re-stamped to msg.
        let out = restamp_wan_user_msg(&um, false).into_owned();
        let p = parse_event(&out).unwrap();
        assert_eq!(p.event_type.as_deref(), Some("msg"));
        assert_eq!(p.from(), Some("playdate-gw"));
        assert_eq!(p.body, "ship it\nnow & later");

        // User-backed origin → kept verbatim (the T5 positive path).
        assert_eq!(restamp_wan_user_msg(&um, true).into_owned(), um);

        // A plain msg envelope is untouched.
        let plain = compose_msg_event("doyle", "hi");
        assert_eq!(restamp_wan_user_msg(&plain, false).into_owned(), plain);

        // A bare (non-envelope) body is untouched.
        assert_eq!(restamp_wan_user_msg("hello", false).into_owned(), "hello");
    }
}

+/// `registry` is the node's [`RegistryHost`]: the user-msg identity gate

+/// resolves the origin endpoint's advertised type from it, keyed by

-pub fn receive_wan(msg: &WanMessage, origin_node: &str, owlery: &Path) -> WanOutcome {


    // user-msg identity gate, WAN-ingress half (REQ-MSG-5; KH 7.5): re-stamp a
    // forged/unverified user-msg down to plain msg before it reaches the target.
    // T4 fail-closes (no WAN origin is yet provably user-backed); T5 resolves a
    // Gateway origin from the registry and threads it here.
    let delivered_body = restamp_wan_user_msg(&msg.body, false);

    // user-msg identity gate, WAN-ingress half (REQ-MSG-5/6; KH 7.5): re-stamp a
    // forged/unverified user-msg down to plain msg before it reaches the target.
    // The origin is provably user-backed iff its advertised endpoint, hosted on
    // the QUIC-proven origin_node, is Gateway-typed (REQ-MSG-6); absent/unknown
    // type fail-closes to a re-stamp.
    let origin_backed = origin_user_backed(&registry.instances_of(&msg.from), origin_node);
    let delivered_body = restamp_wan_user_msg(&msg.body, origin_backed);


    // user-msg identity gate, WAN-ingress half (REQ-MSG-5/6; KH 7.5): re-stamp a
    // forged/unverified user-msg down to plain msg before it reaches the target.
    // The origin is provably user-backed iff its advertised endpoint, hosted on
    // the QUIC-proven origin_node, is Gateway-typed (REQ-MSG-6); absent/unknown
    // type fail-closes to a re-stamp.
    let origin_backed = origin_user_backed(&registry.instances_of(&msg.from), origin_node);
    let delivered_body = restamp_wan_user_msg(&msg.body, origin_backed);

//! WAN message receive funnel (M4-D5a) — wire record → access gate → op-id
//! dedup → the existing local delivery path.
//!
//! The sending node resolved the target (D3c policy), dialed, and wrote
//! [`WanMessage`] NDJSON lines onto a broker-owned QUIC stream (D4). This
//! module is the receiving node's funnel from a decoded record to the local
//! REQ-MSG machinery — it adds exactly two things the local path doesn't have,
//! then delegates:
//!
//! 1. **the access gate** ([`crate::access`], ADR-0009): every wire-inbound
//!    record passes [`access_check`] with the **handshake-proven** origin node
//!    (the broker stream's `remote_id_hex` — never a payload field,
//!    REQ-HAZARD-WAN-ORIGIN-AUTH);
//! 2. **op-id idempotency** (REQ-HAZARD-RESTART-IDEMPOTENT): the wire feed
//!    replays — a sender's journaled redrive, or this brain restarting and
//!    resubscribing from an earlier cursor — so delivery dedups on the
//!    sender-minted durable `op_id`.
//!
//! ## Exactly-once boundaries (honest accounting)
//!
//! - **Spool path (offline target):** fully exactly-once — the op-id claim and
//!   the spool row commit in ONE SQLite transaction
//!   ([`spool::spool_wan_message_at`]); a crash on either side of the write
//!   replays to the same single row.
//! - **Live-TCP path:** check → TCP write → claim. A crash in the gap between
//!   a completed TCP write and the claim re-delivers that op on replay (an
//!   at-least-once window bounded to one in-flight record; the live listener
//!   path has no durable receipt to dedup against). Claim-before-write was
//!   rejected because a crash in ITS gap silently drops the message — for a
//!   messaging substrate, a rare duplicate beats a rare silent drop. Tracked
//!   in `docs/FAULT-MATRIX.md`.

use std::borrow::Cow;
use std::path::Path;

use spt_msg::deliver::{deliver_tcp, perch_exists};
use spt_net::net::registry::Instance;
use spt_net::net::wanmsg::WanMessage;
use spt_proto::endpoint::EndpointType;
use spt_proto::event::{compose_msg_event, is_gateway_endpoint, parse_event, EVENT_TYPE_USER_MSG};
use spt_store::spool;

use crate::access::{access_check, classify_inbound, AccessDecision};
use crate::registryhost::RegistryHost;

/// Re-stamp a WAN-inbound `user-msg` body down to plain `msg` **unless** the
/// transport-proven origin is a user-backed endpoint (CONTEXT §Gateway; the
/// KH 7.5 cross-node enforcement — authority is the proven origin, never the
/// payload). The honored `user-msg` form is a verbatim typed-envelope body, so
/// a forged one would otherwise reach the receiver as user-authority; here we
/// decode it and recompose a plain `msg` envelope, preserving `from` + the inner
/// body. A non-`user-msg` body (a plain message, any other typed envelope, or a
/// bare body) is returned unchanged.
///
/// `origin_user_backed` is resolved by the caller from the proven origin node:
/// over WAN, the ONLY provably user-backed origin is a registry-resolved Gateway
/// endpoint (delivered in T5). Until then callers pass `false`, so every WAN
/// `user-msg` is re-stamped — fail-closed by construction.
// [impl->REQ-MSG-5]
// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
fn restamp_wan_user_msg(body: &str, origin_user_backed: bool) -> Cow<'_, str> {
    if origin_user_backed {
        return Cow::Borrowed(body);
    }
    match parse_event(body) {
        Some(ev) if ev.event_type.as_deref() == Some(EVENT_TYPE_USER_MSG) => {
            Cow::Owned(compose_msg_event(ev.from().unwrap_or(""), &ev.body))
        }
        _ => Cow::Borrowed(body),
    }
}

/// Resolve whether a WAN-inbound `user-msg`'s origin is provably **user-backed**
/// (REQ-MSG-6 — CONTEXT §Gateway): among the origin endpoint's advertised
/// instances, the one hosted on the **QUIC-handshake-proven** `origin_node`
/// advertises the Gateway type ([`spt_proto::event::GATEWAY_TAG`]). Keyed on the
/// proven node, NEVER the wire `from` — a record whose `from` claims a gateway
/// id is inert unless the proven node actually hosts that gateway-typed
/// endpoint. An absent or non-gateway advertised type → `false`: the fail-closed
/// re-stamp (an N-1 node that does not yet advertise `endpoint_type` degrades
/// gracefully to plain `msg`).
///
/// This resolves the *advertised* type; it is **not** a forgery defense. The
/// trust boundary is subnet membership (operator-ratified 2026-06-13): a subnet
/// member is already trusted, so a member forging the Gateway type is out of
/// scope by construction. The proven-node keying is registry-addressing
/// correctness, not an extra trust layer.
// [impl->REQ-MSG-6]
fn origin_user_backed(instances: &[Instance], origin_node: &str) -> bool {
    instances.iter().any(|i| {
        i.node == origin_node
            && i.endpoint_type
                .as_deref()
                .is_some_and(|tag| is_gateway_endpoint(&EndpointType::from_tag(tag)))
    })
}

/// Wall-clock now (epoch ms) — the reply-window clock.
fn now_ms() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0)
}

/// What the funnel did with one inbound record.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WanOutcome {
    /// Delivered live over TCP to the target's listener (op claimed after).
    DeliveredTcp,
    /// Spooled for later drain, atomically with the op claim.
    Spooled,
    /// The op_id was already claimed — a replay; nothing written.
    Duplicate,
    /// Refused at the access gate (ADR-0009) — dropped before any delivery.
    Refused,
    /// No perch for the target on this node — dropped (not claimed: if the
    /// perch appears later, a replayed record may still land).
    NoPerch,
}

/// Funnel one decoded wire record into local delivery. `origin_node` MUST be
/// the QUIC handshake-proven remote node id (the broker's
/// `NetStreamInfo::remote_id_hex` for the stream the record arrived on) —
/// never anything read out of record bytes (REQ-HAZARD-WAN-ORIGIN-AUTH).
/// `registry` is the node's [`RegistryHost`]: the user-msg identity gate
/// resolves the origin endpoint's advertised type from it, keyed by
/// `origin_node` (REQ-MSG-6).
// [impl->REQ-NET-1]
// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
// [impl->REQ-HAZARD-RESTART-IDEMPOTENT]
pub fn receive_wan(
    msg: &WanMessage,
    origin_node: &str,
    owlery: &Path,
    registry: &RegistryHost,
) -> WanOutcome {
    // Gate 2 of ADR-0009 (visibility ran at the sender's resolution; grants
    // gate capabilities, not messages). Messages classify reply-vs-unsolicited
    // against the target's own recent outbound (the stateful firewall): a
    // correlated reply passes a whitelist that would refuse the same node
    // unsolicited.
    let class = classify_inbound(&msg.target, origin_node, now_ms());
    if access_check(&msg.target, origin_node, class) == AccessDecision::Deny {
        return WanOutcome::Refused;
    }

    if !perch_exists(&msg.target) {
        return WanOutcome::NoPerch;
    }

    let perch_path =
        spt_store::perch::resolve_perch_path(&msg.target, spt_store::perch::ParentHint::Infer);

    // Replay check first: a claimed op is done regardless of which path
    // claimed it.
    match spool::wan_seen_at(&perch_path, &msg.op_id) {
        Ok(true) => return WanOutcome::Duplicate,
        Ok(false) => {}
        Err(_) => return WanOutcome::NoPerch,
    }

    // user-msg identity gate, WAN-ingress half (REQ-MSG-5; KH 7.5): re-stamp a
    // forged/unverified user-msg down to plain msg before it reaches the target.
    // T4 fail-closes (no WAN origin is yet provably user-backed); T5 resolves a
    // Gateway origin from the registry and threads it here.
    let delivered_body = restamp_wan_user_msg(&msg.body, false);

    // Live-TCP first (mirrors the local TCP-xor-spool discipline, 4.5), claim
    // after the completed write (see module docs for the window accounting).
    if deliver_tcp(&msg.target, &msg.from, &delivered_body, owlery) {
        let _ = spool::wan_mark_seen_at(&perch_path, &msg.op_id);
        return WanOutcome::DeliveredTcp;
    }

    // Offline: claim + spool in one transaction — exactly-once.
    match spool::spool_wan_message_at(&perch_path, &msg.from, &delivered_body, &msg.op_id) {
        Ok(true) => WanOutcome::Spooled,
        Ok(false) => WanOutcome::Duplicate,
        Err(_) => WanOutcome::NoPerch,
    }
}

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

    // [unit->REQ-MSG-5] WAN-ingress re-stamp (KH 7.5): a user-msg body from an
    // origin not provably user-backed (the T4 fail-closed default) is
    // re-stamped down to plain msg, preserving from + the decoded inner body; a
    // user-backed origin (the T5 Gateway path) keeps it verbatim; a plain msg
    // envelope and a bare body are untouched either way.
    #[test]
    fn wan_user_msg_restamped_unless_origin_user_backed() {
        let um = compose_user_msg_event("playdate-gw", "ship it\nnow & later");

        // Not user-backed → re-stamped to msg.
        let out = restamp_wan_user_msg(&um, false).into_owned();
        let p = parse_event(&out).unwrap();
        assert_eq!(p.event_type.as_deref(), Some("msg"));
        assert_eq!(p.from(), Some("playdate-gw"));
        assert_eq!(p.body, "ship it\nnow & later");

        // User-backed origin → kept verbatim (the T5 positive path).
        assert_eq!(restamp_wan_user_msg(&um, true).into_owned(), um);

        // A plain msg envelope is untouched.
        let plain = compose_msg_event("doyle", "hi");
        assert_eq!(restamp_wan_user_msg(&plain, false).into_owned(), plain);

        // A bare (non-envelope) body is untouched.
        assert_eq!(restamp_wan_user_msg("hello", false).into_owned(), "hello");
    }
}

-    // user-msg identity gate, WAN-ingress half (REQ-MSG-5; KH 7.5): re-stamp a

+    // user-msg identity gate, WAN-ingress half (REQ-MSG-5/6; KH 7.5): re-stamp a

-    // T4 fail-closes (no WAN origin is yet provably user-backed); T5 resolves a

-    // Gateway origin from the registry and threads it here.

-    let delivered_body = restamp_wan_user_msg(&msg.body, false);

+    // The origin is provably user-backed iff its advertised endpoint, hosted on

+    // the QUIC-proven origin_node, is Gateway-typed (REQ-MSG-6); absent/unknown

+    let origin_backed = origin_user_backed(&registry.instances_of(&msg.from), origin_node);

+    let delivered_body = restamp_wan_user_msg(&msg.body, origin_backed);


Update the stale T5 reference in restamp doc.


/// `origin_user_backed` is resolved by the caller from the proven origin node:
/// over WAN, the ONLY provably user-backed origin is a registry-resolved Gateway
/// endpoint (delivered in T5). Until then callers pass `false`, so every WAN
/// `user-msg` is re-stamped — fail-closed by construction.

/// `origin_user_backed` is resolved by the caller from the proven origin node
/// ([`origin_user_backed`]): over WAN, the ONLY provably user-backed origin is a
/// registry-resolved Gateway endpoint advertised on the proven node (REQ-MSG-6).
/// An origin that advertises no type, or a non-Gateway type, yields `false` and
/// the `user-msg` is re-stamped — fail-closed by construction.


/// `origin_user_backed` is resolved by the caller from the proven origin node
/// ([`origin_user_backed`]): over WAN, the ONLY provably user-backed origin is a
/// registry-resolved Gateway endpoint advertised on the proven node (REQ-MSG-6).
/// An origin that advertises no type, or a non-Gateway type, yields `false` and
/// the `user-msg` is re-stamped — fail-closed by construction.

//! WAN message receive funnel (M4-D5a) — wire record → access gate → op-id
//! dedup → the existing local delivery path.
//!
//! The sending node resolved the target (D3c policy), dialed, and wrote
//! [`WanMessage`] NDJSON lines onto a broker-owned QUIC stream (D4). This
//! module is the receiving node's funnel from a decoded record to the local
//! REQ-MSG machinery — it adds exactly two things the local path doesn't have,
//! then delegates:
//!
//! 1. **the access gate** ([`crate::access`], ADR-0009): every wire-inbound
//!    record passes [`access_check`] with the **handshake-proven** origin node
//!    (the broker stream's `remote_id_hex` — never a payload field,
//!    REQ-HAZARD-WAN-ORIGIN-AUTH);
//! 2. **op-id idempotency** (REQ-HAZARD-RESTART-IDEMPOTENT): the wire feed
//!    replays — a sender's journaled redrive, or this brain restarting and
//!    resubscribing from an earlier cursor — so delivery dedups on the
//!    sender-minted durable `op_id`.
//!
//! ## Exactly-once boundaries (honest accounting)
//!
//! - **Spool path (offline target):** fully exactly-once — the op-id claim and
//!   the spool row commit in ONE SQLite transaction
//!   ([`spool::spool_wan_message_at`]); a crash on either side of the write
//!   replays to the same single row.
//! - **Live-TCP path:** check → TCP write → claim. A crash in the gap between
//!   a completed TCP write and the claim re-delivers that op on replay (an
//!   at-least-once window bounded to one in-flight record; the live listener
//!   path has no durable receipt to dedup against). Claim-before-write was
//!   rejected because a crash in ITS gap silently drops the message — for a
//!   messaging substrate, a rare duplicate beats a rare silent drop. Tracked
//!   in `docs/FAULT-MATRIX.md`.

use std::borrow::Cow;
use std::path::Path;

use spt_msg::deliver::{deliver_tcp, perch_exists};
use spt_net::net::registry::Instance;
use spt_net::net::wanmsg::WanMessage;
use spt_proto::endpoint::EndpointType;
use spt_proto::event::{compose_msg_event, is_gateway_endpoint, parse_event, EVENT_TYPE_USER_MSG};
use spt_store::spool;

use crate::access::{access_check, classify_inbound, AccessDecision};
use crate::registryhost::RegistryHost;

/// Re-stamp a WAN-inbound `user-msg` body down to plain `msg` **unless** the
/// transport-proven origin is a user-backed endpoint (CONTEXT §Gateway; the
/// KH 7.5 cross-node enforcement — authority is the proven origin, never the
/// payload). The honored `user-msg` form is a verbatim typed-envelope body, so
/// a forged one would otherwise reach the receiver as user-authority; here we
/// decode it and recompose a plain `msg` envelope, preserving `from` + the inner
/// body. A non-`user-msg` body (a plain message, any other typed envelope, or a
/// bare body) is returned unchanged.
///
/// `origin_user_backed` is resolved by the caller from the proven origin node:
/// over WAN, the ONLY provably user-backed origin is a registry-resolved Gateway
/// endpoint (delivered in T5). Until then callers pass `false`, so every WAN
/// `user-msg` is re-stamped — fail-closed by construction.
// [impl->REQ-MSG-5]
// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
fn restamp_wan_user_msg(body: &str, origin_user_backed: bool) -> Cow<'_, str> {
    if origin_user_backed {
        return Cow::Borrowed(body);
    }
    match parse_event(body) {
        Some(ev) if ev.event_type.as_deref() == Some(EVENT_TYPE_USER_MSG) => {
            Cow::Owned(compose_msg_event(ev.from().unwrap_or(""), &ev.body))
        }
        _ => Cow::Borrowed(body),
    }
}

/// Resolve whether a WAN-inbound `user-msg`'s origin is provably **user-backed**
/// (REQ-MSG-6 — CONTEXT §Gateway): among the origin endpoint's advertised
/// instances, the one hosted on the **QUIC-handshake-proven** `origin_node`
/// advertises the Gateway type ([`spt_proto::event::GATEWAY_TAG`]). Keyed on the
/// proven node, NEVER the wire `from` — a record whose `from` claims a gateway
/// id is inert unless the proven node actually hosts that gateway-typed
/// endpoint. An absent or non-gateway advertised type → `false`: the fail-closed
/// re-stamp (an N-1 node that does not yet advertise `endpoint_type` degrades
/// gracefully to plain `msg`).
///
/// This resolves the *advertised* type; it is **not** a forgery defense. The
/// trust boundary is subnet membership (operator-ratified 2026-06-13): a subnet
/// member is already trusted, so a member forging the Gateway type is out of
/// scope by construction. The proven-node keying is registry-addressing
/// correctness, not an extra trust layer.
// [impl->REQ-MSG-6]
fn origin_user_backed(instances: &[Instance], origin_node: &str) -> bool {
    instances.iter().any(|i| {
        i.node == origin_node
            && i.endpoint_type
                .as_deref()
                .is_some_and(|tag| is_gateway_endpoint(&EndpointType::from_tag(tag)))
    })
}

/// Wall-clock now (epoch ms) — the reply-window clock.
fn now_ms() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0)
}

/// What the funnel did with one inbound record.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WanOutcome {
    /// Delivered live over TCP to the target's listener (op claimed after).
    DeliveredTcp,
    /// Spooled for later drain, atomically with the op claim.
    Spooled,
    /// The op_id was already claimed — a replay; nothing written.
    Duplicate,
    /// Refused at the access gate (ADR-0009) — dropped before any delivery.
    Refused,
    /// No perch for the target on this node — dropped (not claimed: if the
    /// perch appears later, a replayed record may still land).
    NoPerch,
}

/// Funnel one decoded wire record into local delivery. `origin_node` MUST be
/// the QUIC handshake-proven remote node id (the broker's
/// `NetStreamInfo::remote_id_hex` for the stream the record arrived on) —
/// never anything read out of record bytes (REQ-HAZARD-WAN-ORIGIN-AUTH).
/// `registry` is the node's [`RegistryHost`]: the user-msg identity gate
/// resolves the origin endpoint's advertised type from it, keyed by
/// `origin_node` (REQ-MSG-6).
// [impl->REQ-NET-1]
// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
// [impl->REQ-HAZARD-RESTART-IDEMPOTENT]
pub fn receive_wan(
    msg: &WanMessage,
    origin_node: &str,
    owlery: &Path,
    registry: &RegistryHost,
) -> WanOutcome {
    // Gate 2 of ADR-0009 (visibility ran at the sender's resolution; grants
    // gate capabilities, not messages). Messages classify reply-vs-unsolicited
    // against the target's own recent outbound (the stateful firewall): a
    // correlated reply passes a whitelist that would refuse the same node
    // unsolicited.
    let class = classify_inbound(&msg.target, origin_node, now_ms());
    if access_check(&msg.target, origin_node, class) == AccessDecision::Deny {
        return WanOutcome::Refused;
    }

    if !perch_exists(&msg.target) {
        return WanOutcome::NoPerch;
    }

    let perch_path =
        spt_store::perch::resolve_perch_path(&msg.target, spt_store::perch::ParentHint::Infer);

    // Replay check first: a claimed op is done regardless of which path
    // claimed it.
    match spool::wan_seen_at(&perch_path, &msg.op_id) {
        Ok(true) => return WanOutcome::Duplicate,
        Ok(false) => {}
        Err(_) => return WanOutcome::NoPerch,
    }

    // user-msg identity gate, WAN-ingress half (REQ-MSG-5/6; KH 7.5): re-stamp a
    // forged/unverified user-msg down to plain msg before it reaches the target.
    // The origin is provably user-backed iff its advertised endpoint, hosted on
    // the QUIC-proven origin_node, is Gateway-typed (REQ-MSG-6); absent/unknown
    // type fail-closes to a re-stamp.
    let origin_backed = origin_user_backed(&registry.instances_of(&msg.from), origin_node);
    let delivered_body = restamp_wan_user_msg(&msg.body, origin_backed);

    // Live-TCP first (mirrors the local TCP-xor-spool discipline, 4.5), claim
    // after the completed write (see module docs for the window accounting).
    if deliver_tcp(&msg.target, &msg.from, &delivered_body, owlery) {
        let _ = spool::wan_mark_seen_at(&perch_path, &msg.op_id);
        return WanOutcome::DeliveredTcp;
    }

    // Offline: claim + spool in one transaction — exactly-once.
    match spool::spool_wan_message_at(&perch_path, &msg.from, &delivered_body, &msg.op_id) {
        Ok(true) => WanOutcome::Spooled,
        Ok(false) => WanOutcome::Duplicate,
        Err(_) => WanOutcome::NoPerch,
    }
}

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

    // [unit->REQ-MSG-5] WAN-ingress re-stamp (KH 7.5): a user-msg body from an
    // origin not provably user-backed (the T4 fail-closed default) is
    // re-stamped down to plain msg, preserving from + the decoded inner body; a
    // user-backed origin (the T5 Gateway path) keeps it verbatim; a plain msg
    // envelope and a bare body are untouched either way.
    #[test]
    fn wan_user_msg_restamped_unless_origin_user_backed() {
        let um = compose_user_msg_event("playdate-gw", "ship it\nnow & later");

        // Not user-backed → re-stamped to msg.
        let out = restamp_wan_user_msg(&um, false).into_owned();
        let p = parse_event(&out).unwrap();
        assert_eq!(p.event_type.as_deref(), Some("msg"));
        assert_eq!(p.from(), Some("playdate-gw"));
        assert_eq!(p.body, "ship it\nnow & later");

        // User-backed origin → kept verbatim (the T5 positive path).
        assert_eq!(restamp_wan_user_msg(&um, true).into_owned(), um);

        // A plain msg envelope is untouched.
        let plain = compose_msg_event("doyle", "hi");
        assert_eq!(restamp_wan_user_msg(&plain, false).into_owned(), plain);

        // A bare (non-envelope) body is untouched.
        assert_eq!(restamp_wan_user_msg("hello", false).into_owned(), "hello");
    }
}

-/// `origin_user_backed` is resolved by the caller from the proven origin node:

-/// over WAN, the ONLY provably user-backed origin is a registry-resolved Gateway

-/// endpoint (delivered in T5). Until then callers pass `false`, so every WAN

-/// `user-msg` is re-stamped — fail-closed by construction.

+/// `origin_user_backed` is resolved by the caller from the proven origin node

+/// ([`origin_user_backed`]): over WAN, the ONLY provably user-backed origin is a

+/// registry-resolved Gateway endpoint advertised on the proven node (REQ-MSG-6).

+/// An origin that advertises no type, or a non-Gateway type, yields `false` and

+/// the `user-msg` is re-stamped — fail-closed by construction.


REQ survey + mint REQ-MSG-6

Add REQ-MSG-6 to traceable-reqs.toml [doc,impl,unit]; update REQ-EP-6 title (drop deferred clause); note REQ-INST-7 model extension. Activate stages as satisfied.

T1 Instance.endpoint_type field

Add endpoint_type: Option<String> to Instance (additive serde-default, skip_serializing_if none). Update all ~24 construction sites with endpoint_type: None. impl->REQ-MSG-6.

T2 populate on advertise_local

In advertise_local, set endpoint_type from info.state (the endpoint's own record). Node advertises only its own endpoints' types.

T3 resolve at receive_wan funnel

Pure helper origin_user_backed(instances, origin_node); RegistryHost::instances_of(id); thread &RegistryHost into receive_wan + serve_wan_feed; flip hardcoded false. Update test callers.

T4 unit test matrix

5-row matrix on origin_user_backed + restamp. gateway/agent/none/wrong-node/plain-msg.

Sweep + traceable + push

Full workspace test, clippy -D, --no-default-features, xtask check, traceable EXIT=0, schema re-bless if needed. Push, CI both runners, ping doyle.


Now thread `registry` through `serve_wan_feed` in dispatch.rs.


StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths),

StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths, registry),


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

StreamFamily::Notif => serve_notif_feed(&mut brain, stream_id, origin),

-        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths),

+        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths, registry),

StreamFamily::Registry => serve_registry_feed(&mut brain, stream_id, origin, registry),

// The gate subject is the session's OWNING endpoint, resolved


fn serve_wan_feed(
    brain: &mut Brain,
    stream_id: u64,
    origin: &str,
    paths: &DispatchPaths,
) -> DispatchOutcome {

fn serve_wan_feed(
    brain: &mut Brain,
    stream_id: u64,
    origin: &str,
    paths: &DispatchPaths,
    registry: &RegistryHost,
) -> DispatchOutcome {


fn serve_wan_feed(
    brain: &mut Brain,
    stream_id: u64,
    origin: &str,
    paths: &DispatchPaths,
) -> DispatchOutcome {

fn serve_wan_feed(
    brain: &mut Brain,
    stream_id: u64,
    origin: &str,
    paths: &DispatchPaths,
    registry: &RegistryHost,
) -> DispatchOutcome {

if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {


                for msg in decoder.push(&bytes) {
                    let _ = receive_wan(&msg, origin, &paths.owlery);
                    handled += 1;
                }

                for msg in decoder.push(&bytes) {
                    let _ = receive_wan(&msg, origin, &paths.owlery, registry);
                    handled += 1;
                }


                for msg in decoder.push(&bytes) {
                    let _ = receive_wan(&msg, origin, &paths.owlery);
                    handled += 1;
                }

                for msg in decoder.push(&bytes) {
                    let _ = receive_wan(&msg, origin, &paths.owlery, registry);
                    handled += 1;
                }

-                    let _ = receive_wan(&msg, origin, &paths.owlery);

+                    let _ = receive_wan(&msg, origin, &paths.owlery, registry);


Now the test callers — access.rs:119 and wanmsg.rs:141,175,324.


100
101	/// Read events until exactly one decoded record has been funneled; return its
102	/// outcome. Stepwise (one record per call) so the test can edit the access
103	/// store between deliveries.
104	fn funnel_one(
105	    brain: &mut Brain,
106	    stream_id: u64,
107	    decoder: &mut WanDecoder,
108	    origin: &str,
109	    owlery: &std::path::Path,
110	) -> WanOutcome {
111	    loop {
112	        match brain.read_event().expect("read event") {
113	            BrokerEvent::NetStreamData {
114	                stream_id: sid,
115	                bytes,
116	                ..
117	            } if sid == stream_id => {
118	                if let Some(rec) = decoder.push(&bytes).into_iter().next() {
119	                    return receive_wan(&rec, origin, owlery);
120	                }
121	            }
122	            BrokerEvent::NetStreamEof { .. } => panic!("eof before the record arrived"),
123	            BrokerEvent::Error { message } => panic!("broker error: {message}"),
124	            _ => {}
125	        }
126	    }
127	}
128
129	fn msg(target: &str, body: &str, op_id: &str) -> WanMessage {

/// Read events until exactly one decoded record has been funneled; return its
/// outcome. Stepwise (one record per call) so the test can edit the access
/// store between deliveries.
fn funnel_one(
    brain: &mut Brain,
    stream_id: u64,
    decoder: &mut WanDecoder,
    origin: &str,
    owlery: &std::path::Path,
) -> WanOutcome {
    loop {
        match brain.read_event().expect("read event") {
            BrokerEvent::NetStreamData {
                stream_id: sid,
                bytes,
                ..
            } if sid == stream_id => {
                if let Some(rec) = decoder.push(&bytes).into_iter().next() {
                    return receive_wan(&rec, origin, owlery);
                }
            }
            BrokerEvent::NetStreamEof { .. } => panic!("eof before the record arrived"),
            BrokerEvent::Error { message } => panic!("broker error: {message}"),
            _ => {}
        }
    }
}

fn msg(target: &str, body: &str, op_id: &str) -> WanMessage {


Bash

grep -n "funnel_one\|EpochSource\|RegistryHost\|with_home\|tempdir\|fn .*test\|owlery\b" crates/spt-daemon/tests/access.rs | head -40

Map access.rs test structure


104:fn funnel_one(
109:    owlery: &std::path::Path,
119:                    return receive_wan(&rec, origin, owlery);
148:    let dir = tempfile::tempdir().expect("tempdir");
155:    let owlery = perch::owlery_dir();
191:        funnel_one(&mut receiver, stream, &mut decoder, &origin, &owlery),
209:        funnel_one(&mut receiver, stream, &mut decoder, &origin, &owlery),
224:        funnel_one(&mut receiver, stream, &mut decoder, &origin, &owlery),


144	// restart.
145	#[test]
146	fn whitelist_refuses_exempts_replies_and_admits_listed() {
147	    init_home();
148	    let dir = tempfile::tempdir().expect("tempdir");
149	    let (name_a, name_b) = (unique_name(), unique_name());
150	    let _broker_a = net_broker(&name_a, &dir.path().join("a"));
151	    let _broker_b = net_broker(&name_b, &dir.path().join("b"));
152
153	    let target = unique_id("ling");
154	    let perch_path = offline_perch(&target);
155	    let owlery = perch::owlery_dir();
156
157	    // Restrict the target BEFORE any traffic: whitelist names some other node,
158	    // so sender A is non-listed.
159	    let mut store = AccessStore::load();
160	    store.allow(&target, &"f".repeat(64));
161	    store.save().expect("save access store");
162
163	    // Node A dials B and opens the message stream.
164	    let mut sender = connect_retry(&name_a);
165	    let a_node = sender
166	        .net_status()
167	        .expect("a status")
168	        .node_id_hex
169	        .expect("node id");
170	    let mut receiver = connect_retry(&name_b);
171	    let addr = receiver.net_status().expect("b status").addr;
172	    let conn = sender.net_dial(addr, Some(1)).expect("dial");
173	    let opened = sender.net_open_stream(conn.conn_id, Some(2)).expect("open");
174
175	    // (1) Unsolicited from a non-listed node → refused at the gate, no spool.
176	    // (Sent before the stream-wait: a QUIC bidi stream only becomes visible to
177	    // the acceptor once bytes flow.)
178	    let m1 = msg(&target, "let me in", "a:1");
179	    sender
180	        .net_stream_send(opened.stream_id, &m1.encode_line(), Some(100), false)
181	        .expect("send 1");
182
183	    let (stream, origin) = wait_for_stream(&mut receiver);
184	    assert_eq!(
185	        origin, a_node,
186	        "gate subject IS the QUIC handshake identity"
187	    );
188	    receiver.net_stream_subscribe(stream, 0).expect("subscribe");
189	    let mut decoder = WanDecoder::new();
190	    assert_eq!(
191	        funnel_one(&mut receiver, stream, &mut decoder, &origin, &owlery),
192	        WanOutcome::Refused,
193	        "non-listed unsolicited refused"

// restart.
#[test]
fn whitelist_refuses_exempts_replies_and_admits_listed() {
    init_home();
    let dir = tempfile::tempdir().expect("tempdir");
    let (name_a, name_b) = (unique_name(), unique_name());
    let _broker_a = net_broker(&name_a, &dir.path().join("a"));
    let _broker_b = net_broker(&name_b, &dir.path().join("b"));

    let target = unique_id("ling");
    let perch_path = offline_perch(&target);
    let owlery = perch::owlery_dir();

    // Restrict the target BEFORE any traffic: whitelist names some other node,
    // so sender A is non-listed.
    let mut store = AccessStore::load();
    store.allow(&target, &"f".repeat(64));
    store.save().expect("save access store");

    // Node A dials B and opens the message stream.
    let mut sender = connect_retry(&name_a);
    let a_node = sender
        .net_status()
        .expect("a status")
        .node_id_hex
        .expect("node id");
    let mut receiver = connect_retry(&name_b);
    let addr = receiver.net_status().expect("b status").addr;
    let conn = sender.net_dial(addr, Some(1)).expect("dial");
    let opened = sender.net_open_stream(conn.conn_id, Some(2)).expect("open");

    // (1) Unsolicited from a non-listed node → refused at the gate, no spool.
    // (Sent before the stream-wait: a QUIC bidi stream only becomes visible to
    // the acceptor once bytes flow.)
    let m1 = msg(&target, "let me in", "a:1");
    sender
        .net_stream_send(opened.stream_id, &m1.encode_line(), Some(100), false)
        .expect("send 1");

    let (stream, origin) = wait_for_stream(&mut receiver);
    assert_eq!(
        origin, a_node,
        "gate subject IS the QUIC handshake identity"
    );
    receiver.net_stream_subscribe(stream, 0).expect("subscribe");
    let mut decoder = WanDecoder::new();
    assert_eq!(
        funnel_one(&mut receiver, stream, &mut decoder, &origin, &owlery),
        WanOutcome::Refused,
        "non-listed unsolicited refused"


grep -n "^use\|EpochSource" crates/spt-daemon/tests/access.rs | head -30

access.rs imports


13:use std::sync::atomic::{AtomicU32, Ordering};
14:use std::sync::{Arc, OnceLock};
15:use std::thread;
16:use std::time::Duration;
18:use spt_daemon::access::note_outbound;
19:use spt_daemon::brain::{Brain, BrokerEvent};
20:use spt_daemon::nethost::{NetConfig, NetHost};
21:use spt_daemon::wan::{receive_wan, WanOutcome};
22:use spt_daemon::Broker;
23:use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
24:use spt_net::net::wanmsg::{WanDecoder, WanMessage};
25:use spt_proto::identity::Identity;
26:use spt_store::access::AccessStore;
27:use spt_store::info::{self, InfoJson};
28:use spt_store::perch::{self, ParentHint};
29:use spt_store::spool;
30:use tempfile::TempDir;


grep -rn "use.*EpochSource\|pub use.*EpochSource\|EpochSource" crates/spt-daemon/src/registryhost.rs | head -3; echo "==="; grep -rn "RegistryHost::new\b\|RegistryHost::new(" crates/spt-daemon/tests/*.rs | head -3

Find EpochSource import + RegistryHost::new test usage


16://!   [`EpochSource`] epoch — the per-node monotonic lease key, never
38:use spt_store::epoch::EpochSource;
117:    epoch: Mutex<EpochSource>,
===
crates/spt-daemon/tests/dispatch.rs:106:    let registry = Arc::new(spt_daemon::registryhost::RegistryHost::new("b-dispatch"));
crates/spt-daemon/tests/twohost.rs:305:    // `RegistryHost::new` uses): the presence fire path + feed-apply surface


REQ survey + mint REQ-MSG-6

T1 Instance.endpoint_type field

T2 populate on advertise_local

T3 resolve at receive_wan funnel

T4 unit test matrix

Sweep + traceable + push


access.rs: add registry param to funnel_one + construct via `RegistryHost::new`.


Edit

use spt_daemon::nethost::{NetConfig, NetHost};
use spt_daemon::wan::{receive_wan, WanOutcome};

use spt_daemon::nethost::{NetConfig, NetHost};
use spt_daemon::registryhost::RegistryHost;
use spt_daemon::wan::{receive_wan, WanOutcome};


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

use spt_daemon::nethost::{NetConfig, NetHost};
use spt_daemon::registryhost::RegistryHost;
use spt_daemon::wan::{receive_wan, WanOutcome};

//! D5d regression — endpoint access whitelist over the real wire (REQ-SEC-1,
//! ADR-0009): the receiving daemon's gate, under the handshake-proven origin,
//! refuses a non-listed node's unsolicited message, exempts a correlated
//! reply (stateful firewall), and admits a whitelisted node — with edits to
//! the store taking effect mid-stream (the gate re-reads per check).
//!
//! Two in-process brokers (hermetic loopback endpoints) play the two daemons,
//! the same harness as `wanmsg.rs`. The payload-origin spoof negative lives
//! in `wanmsg.rs` (KH 7.5); the same-node fast path is unit-tested in
//! `src/access.rs` (the hermetic broker identity is deliberately not the
//! store's node.key).

use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, OnceLock};
use std::thread;
use std::time::Duration;

use spt_daemon::access::note_outbound;
use spt_daemon::brain::{Brain, BrokerEvent};
use spt_daemon::nethost::{NetConfig, NetHost};
use spt_daemon::wan::{receive_wan, WanOutcome};
use spt_daemon::Broker;
use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
use spt_net::net::wanmsg::{WanDecoder, WanMessage};
use spt_proto::identity::Identity;
use spt_store::access::AccessStore;
use spt_store::info::{self, InfoJson};
use spt_store::perch::{self, ParentHint};
use spt_store::spool;
use tempfile::TempDir;

static HOME: OnceLock<TempDir> = OnceLock::new();
fn init_home() {
    HOME.get_or_init(|| {
        let dir = TempDir::new().expect("temp dir");
        std::env::set_var("SPT_HOME", dir.path());
        dir
    });
}

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

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

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

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

fn wait_for_stream(brain: &mut Brain) -> (u64, String) {
    for _ in 0..400 {
        let reply = brain.net_streams().expect("net-streams");
        if let Some(s) = reply.streams.first() {
            return (s.stream_id, s.remote_id_hex.clone());
        }
        thread::sleep(Duration::from_millis(5));
    }
    panic!("stream never appeared at the receiver's broker");
}

fn offline_perch(id: &str) -> std::path::PathBuf {
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    std::fs::create_dir_all(&perch_path).expect("perch dir");
    let rec = InfoJson::new(id, "now", std::process::id(), "sess", "ready_agent");
    info::write_info(&perch_path, &rec).expect("info.json");
    perch_path
}

/// Read events until exactly one decoded record has been funneled; return its
/// outcome. Stepwise (one record per call) so the test can edit the access
/// store between deliveries.
fn funnel_one(
    brain: &mut Brain,
    stream_id: u64,
    decoder: &mut WanDecoder,
    origin: &str,
    owlery: &std::path::Path,
) -> WanOutcome {
    loop {
        match brain.read_event().expect("read event") {
            BrokerEvent::NetStreamData {
                stream_id: sid,
                bytes,
                ..
            } if sid == stream_id => {
                if let Some(rec) = decoder.push(&bytes).into_iter().next() {
                    return receive_wan(&rec, origin, owlery);
                }
            }
            BrokerEvent::NetStreamEof { .. } => panic!("eof before the record arrived"),
            BrokerEvent::Error { message } => panic!("broker error: {message}"),
            _ => {}
        }
    }
}

fn msg(target: &str, body: &str, op_id: &str) -> WanMessage {
    WanMessage {
        target: target.into(),
        from: "doyle".into(),
        body: body.into(),
        op_id: op_id.into(),
    }
}

// [unit->REQ-SEC-1] the whitelist gate over the real wire, under the
// handshake-proven origin: (1) a restricted endpoint REFUSES a non-listed
// node's unsolicited message — nothing spools; (2) the same node passes once
// the endpoint's own recent outbound names it (reply exemption — stateful
// firewall, the whitelist untouched); (3) whitelisting the node admits its
// unsolicited traffic again — store edits take effect mid-stream, no daemon
// restart.
#[test]
fn whitelist_refuses_exempts_replies_and_admits_listed() {
    init_home();
    let dir = tempfile::tempdir().expect("tempdir");
    let (name_a, name_b) = (unique_name(), unique_name());
    let _broker_a = net_broker(&name_a, &dir.path().join("a"));
    let _broker_b = net_broker(&name_b, &dir.path().join("b"));

    let target = unique_id("ling");
    let perch_path = offline_perch(&target);
    let owlery = perch::owlery_dir();

    // Restrict the target BEFORE any traffic: whitelist names some other node,
    // so sender A is non-listed.
    let mut store = AccessStore::load();
    store.allow(&target, &"f".repeat(64));
    store.save().expect("save access store");

    // Node A dials B and opens the message stream.
    let mut sender = connect_retry(&name_a);
    let a_node = sender
        .net_status()
        .expect("a status")
        .node_id_hex
        .expect("node id");
    let mut receiver = connect_retry(&name_b);
    let addr = receiver.net_status().expect("b status").addr;
    let conn = sender.net_dial(addr, Some(1)).expect("dial");
    let opened = sender.net_open_stream(conn.conn_id, Some(2)).expect("open");

    // (1) Unsolicited from a non-listed node → refused at the gate, no spool.
    // (Sent before the stream-wait: a QUIC bidi stream only becomes visible to
    // the acceptor once bytes flow.)
    let m1 = msg(&target, "let me in", "a:1");
    sender
        .net_stream_send(opened.stream_id, &m1.encode_line(), Some(100), false)
        .expect("send 1");

    let (stream, origin) = wait_for_stream(&mut receiver);
    assert_eq!(
        origin, a_node,
        "gate subject IS the QUIC handshake identity"
    );
    receiver.net_stream_subscribe(stream, 0).expect("subscribe");
    let mut decoder = WanDecoder::new();
    assert_eq!(
        funnel_one(&mut receiver, stream, &mut decoder, &origin, &owlery),
        WanOutcome::Refused,
        "non-listed unsolicited refused"
    );
    assert_eq!(
        spool::pending_count_at(&perch_path).expect("count"),
        0,
        "nothing spooled"
    );

    // (2) The endpoint itself speaks to node A; A's next message classifies as
    // a correlated reply and passes the SAME whitelist (stateful firewall).
    note_outbound(&target, &a_node, now_ms());
    let m2 = msg(&target, "re: your message", "a:2");
    sender
        .net_stream_send(opened.stream_id, &m2.encode_line(), Some(101), false)
        .expect("send 2");
    assert_eq!(
        funnel_one(&mut receiver, stream, &mut decoder, &origin, &owlery),
        WanOutcome::Spooled,
        "correlated reply exempt from the whitelist"
    );

    // (3) Whitelist node A; its unsolicited traffic now passes — the gate
    // re-read the store mid-stream (no restart, no resubscribe).
    let mut store = AccessStore::load();
    store.allow(&target, &a_node);
    store.save().expect("save access store");
    let m3 = msg(&target, "hello again, listed now", "a:3");
    sender
        .net_stream_send(opened.stream_id, &m3.encode_line(), Some(102), true)
        .expect("send 3");
    assert_eq!(
        funnel_one(&mut receiver, stream, &mut decoder, &origin, &owlery),
        WanOutcome::Spooled,
        "whitelisted node admitted"
    );

    let pending = spool::peek_all_at(&perch_path).expect("peek");
    assert_eq!(
        pending.len(),
        2,
        "exactly the reply + the whitelisted message landed"
    );
    assert_eq!(pending[0].2, "re: your message");
    assert_eq!(pending[1].2, "hello again, listed now");
}

fn now_ms() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0)
}


fn funnel_one(
    brain: &mut Brain,
    stream_id: u64,
    decoder: &mut WanDecoder,
    origin: &str,
    owlery: &std::path::Path,
) -> WanOutcome {
    loop {
        match brain.read_event().expect("read event") {
            BrokerEvent::NetStreamData {
                stream_id: sid,
                bytes,
                ..
            } if sid == stream_id => {
                if let Some(rec) = decoder.push(&bytes).into_iter().next() {
                    return receive_wan(&rec, origin, owlery);
                }
            }

fn funnel_one(
    brain: &mut Brain,
    stream_id: u64,
    decoder: &mut WanDecoder,
    origin: &str,
    owlery: &std::path::Path,
    registry: &RegistryHost,
) -> WanOutcome {
    loop {
        match brain.read_event().expect("read event") {
            BrokerEvent::NetStreamData {
                stream_id: sid,
                bytes,
                ..
            } if sid == stream_id => {
                if let Some(rec) = decoder.push(&bytes).into_iter().next() {
                    return receive_wan(&rec, origin, owlery, registry);
                }
            }


fn funnel_one(
    brain: &mut Brain,
    stream_id: u64,
    decoder: &mut WanDecoder,
    origin: &str,
    owlery: &std::path::Path,
    registry: &RegistryHost,
) -> WanOutcome {
    loop {
        match brain.read_event().expect("read event") {
            BrokerEvent::NetStreamData {
                stream_id: sid,
                bytes,
                ..
            } if sid == stream_id => {
                if let Some(rec) = decoder.push(&bytes).into_iter().next() {
                    return receive_wan(&rec, origin, owlery, registry);
                }
            }

//! D5d regression — endpoint access whitelist over the real wire (REQ-SEC-1,
//! ADR-0009): the receiving daemon's gate, under the handshake-proven origin,
//! refuses a non-listed node's unsolicited message, exempts a correlated
//! reply (stateful firewall), and admits a whitelisted node — with edits to
//! the store taking effect mid-stream (the gate re-reads per check).
//!
//! Two in-process brokers (hermetic loopback endpoints) play the two daemons,
//! the same harness as `wanmsg.rs`. The payload-origin spoof negative lives
//! in `wanmsg.rs` (KH 7.5); the same-node fast path is unit-tested in
//! `src/access.rs` (the hermetic broker identity is deliberately not the
//! store's node.key).

use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, OnceLock};
use std::thread;
use std::time::Duration;

use spt_daemon::access::note_outbound;
use spt_daemon::brain::{Brain, BrokerEvent};
use spt_daemon::nethost::{NetConfig, NetHost};
use spt_daemon::registryhost::RegistryHost;
use spt_daemon::wan::{receive_wan, WanOutcome};
use spt_daemon::Broker;
use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
use spt_net::net::wanmsg::{WanDecoder, WanMessage};
use spt_proto::identity::Identity;
use spt_store::access::AccessStore;
use spt_store::info::{self, InfoJson};
use spt_store::perch::{self, ParentHint};
use spt_store::spool;
use tempfile::TempDir;

static HOME: OnceLock<TempDir> = OnceLock::new();
fn init_home() {
    HOME.get_or_init(|| {
        let dir = TempDir::new().expect("temp dir");
        std::env::set_var("SPT_HOME", dir.path());
        dir
    });
}

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

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

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

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

fn wait_for_stream(brain: &mut Brain) -> (u64, String) {
    for _ in 0..400 {
        let reply = brain.net_streams().expect("net-streams");
        if let Some(s) = reply.streams.first() {
            return (s.stream_id, s.remote_id_hex.clone());
        }
        thread::sleep(Duration::from_millis(5));
    }
    panic!("stream never appeared at the receiver's broker");
}

fn offline_perch(id: &str) -> std::path::PathBuf {
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    std::fs::create_dir_all(&perch_path).expect("perch dir");
    let rec = InfoJson::new(id, "now", std::process::id(), "sess", "ready_agent");
    info::write_info(&perch_path, &rec).expect("info.json");
    perch_path
}

/// Read events until exactly one decoded record has been funneled; return its
/// outcome. Stepwise (one record per call) so the test can edit the access
/// store between deliveries.
fn funnel_one(
    brain: &mut Brain,
    stream_id: u64,
    decoder: &mut WanDecoder,
    origin: &str,
    owlery: &std::path::Path,
) -> WanOutcome {
    loop {
        match brain.read_event().expect("read event") {
            BrokerEvent::NetStreamData {
                stream_id: sid,
                bytes,
                ..
            } if sid == stream_id => {
                if let Some(rec) = decoder.push(&bytes).into_iter().next() {
                    return receive_wan(&rec, origin, owlery);
                }
            }
            BrokerEvent::NetStreamEof { .. } => panic!("eof before the record arrived"),
            BrokerEvent::Error { message } => panic!("broker error: {message}"),
            _ => {}
        }
    }
}

fn msg(target: &str, body: &str, op_id: &str) -> WanMessage {
    WanMessage {
        target: target.into(),
        from: "doyle".into(),
        body: body.into(),
        op_id: op_id.into(),
    }
}

// [unit->REQ-SEC-1] the whitelist gate over the real wire, under the
// handshake-proven origin: (1) a restricted endpoint REFUSES a non-listed
// node's unsolicited message — nothing spools; (2) the same node passes once
// the endpoint's own recent outbound names it (reply exemption — stateful
// firewall, the whitelist untouched); (3) whitelisting the node admits its
// unsolicited traffic again — store edits take effect mid-stream, no daemon
// restart.
#[test]
fn whitelist_refuses_exempts_replies_and_admits_listed() {
    init_home();
    let dir = tempfile::tempdir().expect("tempdir");
    let (name_a, name_b) = (unique_name(), unique_name());
    let _broker_a = net_broker(&name_a, &dir.path().join("a"));
    let _broker_b = net_broker(&name_b, &dir.path().join("b"));

    let target = unique_id("ling");
    let perch_path = offline_perch(&target);
    let owlery = perch::owlery_dir();

    // Restrict the target BEFORE any traffic: whitelist names some other node,
    // so sender A is non-listed.
    let mut store = AccessStore::load();
    store.allow(&target, &"f".repeat(64));
    store.save().expect("save access store");

    // Node A dials B and opens the message stream.
    let mut sender = connect_retry(&name_a);
    let a_node = sender
        .net_status()
        .expect("a status")
        .node_id_hex
        .expect("node id");
    let mut receiver = connect_retry(&name_b);
    let addr = receiver.net_status().expect("b status").addr;
    let conn = sender.net_dial(addr, Some(1)).expect("dial");
    let opened = sender.net_open_stream(conn.conn_id, Some(2)).expect("open");

    // (1) Unsolicited from a non-listed node → refused at the gate, no spool.
    // (Sent before the stream-wait: a QUIC bidi stream only becomes visible to
    // the acceptor once bytes flow.)
    let m1 = msg(&target, "let me in", "a:1");
    sender
        .net_stream_send(opened.stream_id, &m1.encode_line(), Some(100), false)
        .expect("send 1");

    let (stream, origin) = wait_for_stream(&mut receiver);
    assert_eq!(
        origin, a_node,
        "gate subject IS the QUIC handshake identity"
    );
    receiver.net_stream_subscribe(stream, 0).expect("subscribe");
    let mut decoder = WanDecoder::new();
    assert_eq!(
        funnel_one(&mut receiver, stream, &mut decoder, &origin, &owlery),
        WanOutcome::Refused,
        "non-listed unsolicited refused"
    );
    assert_eq!(
        spool::pending_count_at(&perch_path).expect("count"),
        0,
        "nothing spooled"
    );

    // (2) The endpoint itself speaks to node A; A's next message classifies as
    // a correlated reply and passes the SAME whitelist (stateful firewall).
    note_outbound(&target, &a_node, now_ms());
    let m2 = msg(&target, "re: your message", "a:2");
    sender
        .net_stream_send(opened.stream_id, &m2.encode_line(), Some(101), false)
        .expect("send 2");
    assert_eq!(
        funnel_one(&mut receiver, stream, &mut decoder, &origin, &owlery),
        WanOutcome::Spooled,
        "correlated reply exempt from the whitelist"
    );

    // (3) Whitelist node A; its unsolicited traffic now passes — the gate
    // re-read the store mid-stream (no restart, no resubscribe).
    let mut store = AccessStore::load();
    store.allow(&target, &a_node);
    store.save().expect("save access store");
    let m3 = msg(&target, "hello again, listed now", "a:3");
    sender
        .net_stream_send(opened.stream_id, &m3.encode_line(), Some(102), true)
        .expect("send 3");
    assert_eq!(
        funnel_one(&mut receiver, stream, &mut decoder, &origin, &owlery),
        WanOutcome::Spooled,
        "whitelisted node admitted"
    );

    let pending = spool::peek_all_at(&perch_path).expect("peek");
    assert_eq!(
        pending.len(),
        2,
        "exactly the reply + the whitelisted message landed"
    );
    assert_eq!(pending[0].2, "re: your message");
    assert_eq!(pending[1].2, "hello again, listed now");
}

fn now_ms() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0)
}

-                    return receive_wan(&rec, origin, owlery);

+                    return receive_wan(&rec, origin, owlery, registry);


    let owlery = perch::owlery_dir();

    // Restrict the target BEFORE any traffic:

    let owlery = perch::owlery_dir();
    // An empty registry: no origin advertises a Gateway type, so the user-msg
    // gate fail-closes — these tests assert the access gate, not the type gate.
    let registry = RegistryHost::new("b-access");

    // Restrict the target BEFORE any traffic: