Now find GATEWAY_TAG, classify_local_origin, advertise_local, and all `Instance {` construction sites.


adapters\mock\src\main.rs:53:    let endpoint_type = arg(&args, "--type");
adapters\mock\src\main.rs:66:    if let Some(ty) = &endpoint_type {
CONTEXT.md:213:[Omitted long matching line]
crates\spt-proto\src\event.rs:94:pub const GATEWAY_TAG: &str = "gateway";
crates\spt-proto\src\event.rs:115:/// Whether an endpoint type is a Gateway (the open-type [`GATEWAY_TAG`]). A
crates\spt-proto\src\event.rs:120:    matches!(ty, EndpointType::Other(tag) if tag == GATEWAY_TAG)
crates\spt-proto\src\event.rs:450:        let gateway = MsgOrigin::Endpoint(EndpointType::from_tag(GATEWAY_TAG));
crates\spt\src\api\mod.rs:88:        endpoint_type: String,
crates\spt\src\api\mod.rs:330:            endpoint_type,
crates\spt\src\api\mod.rs:342:                &endpoint_type,
crates\spt\src\cli.rs:2167:fn classify_local_origin(self_id: Option<&str>) -> MsgOrigin {
crates\spt\src\cli.rs:2196:    let origin = classify_local_origin(self_id);
crates\spt\src\api\startup.rs:153:    endpoint_type: &str,
crates\spt\src\api\startup.rs:178:    let endpoint_type = prior
crates\spt\src\api\startup.rs:181:        .filter(|s| endpoint_type == "live_agent" && !s.is_empty())
crates\spt\src\api\startup.rs:182:        .unwrap_or_else(|| endpoint_type.to_string());
crates\spt\src\api\startup.rs:188:        &endpoint_type,
crates\spt\src\api\startup.rs:332:    endpoint_type: &str,
crates\spt\src\api\startup.rs:349:    match establish_perch(id, &session_id, parent_pid, adapter, subnet, endpoint_type) {
crates\spt-daemon\src\registryhost.rs:329:    pub fn advertise_local(&self, owlery: &Path) -> Vec<RegistryFeedRecord> {
crates\spt-daemon\src\registryhost.rs:1386:    fn advertise_local_scans_and_respects_visibility() {
crates\spt-daemon\src\registryhost.rs:1580:    fn advertise_local_stamps_the_node_label() {
crates\spt-daemon\src\shellhost.rs:534:        // A Gateway-typed owner perch (state="gateway"), bound like any Self perch.
crates\spt\tests\gateway_e2e.rs:8://!      which fires `api bind --type gateway` → the perch binds `state="gateway"`
M9-GATEWAY-WAN-PLAN.md:37:  advertised `endpoint_type`" `[doc,impl,unit]`. Doc stage = the CONTEXT posture
M9-GATEWAY-WAN-PLAN.md:40:- The `Instance.endpoint_type` field extends **REQ-INST-7**'s data model — add
M9-GATEWAY-WAN-PLAN.md:50:**T1 — `Instance.endpoint_type` (impl).** Add `endpoint_type: Option<String>`
M9-GATEWAY-WAN-PLAN.md:52:tag string to keep the taxonomy open, mirroring `GATEWAY_TAG`) to
M9-GATEWAY-WAN-PLAN.md:60:`Instance` for a local endpoint, set `endpoint_type` from the endpoint's own
M9-GATEWAY-WAN-PLAN.md:61:record (the info.json type / `state="gateway"` that T4's `classify_local_origin`
M9-GATEWAY-WAN-PLAN.md:67:`endpoint_type` from the registry **keyed by the QUIC-proven `origin_node`** (the
M9-GATEWAY-WAN-PLAN.md:70:keying). Derive `origin_user_backed = endpoint_type advertises GATEWAY_TAG` and
M9-GATEWAY-WAN-PLAN.md:77:| `endpoint_type=gateway`, hosted on proven node | `user-msg` | **honored** (stays `user-msg`) |
M9-GATEWAY-WAN-PLAN.md:78:| `endpoint_type=<agent/other>` | `user-msg` | re-stamped to `msg` |
M9-GATEWAY-WAN-PLAN.md:79:| no `endpoint_type` advertised (N-1 node) | `user-msg` | re-stamped (rollout grace) |
ROADMAP.md:81:[Omitted long matching line]
traceable-reqs.toml:175:[Omitted long matching line]


Found 30 files
M9-GATEWAY-WAN-PLAN.md
crates\spt\src\cli.rs
traceable-reqs.toml
crates\spt-proto\src\event.rs
crates\spt-daemon\src\pump\mod.rs
crates\spt-daemon\src\notif.rs
crates\spt-daemon\tests\pump.rs
crates\spt-daemon\src\pump\registry.rs
PUMP-SEAM-PLAN.md
crates\spt-daemon\src\lifecycle.rs
crates\spt-daemon\tests\dispatch.rs
docs\adr\0017-subnet-mesh-membership-seed-proof.md
crates\spt\src\wansend.rs
crates\spt-net\src\net\wanmsg.rs
crates\spt-net\src\net\replicate.rs
crates\spt-net\src\net\registry.rs
crates\spt-daemon\tests\sync.rs
crates\spt-daemon\tests\replicate.rs
crates\spt-daemon\src\shelldisc.rs
crates\spt-daemon\src\registryhost.rs
crates\spt-daemon\src\reconcile.rs
crates\spt-daemon\src\presence.rs
SUBNET-MESH-PLAN.md
MESH-D4-PLAN.md
MESH-D5-PLAN.md
MESH-D6-PLAN.md
M8-PLAN.md
MESH-D0-PLAN.md
M5-D6-PLAN.md
M4-D9-PLAN.md


Read event.rs EndpointType, cli.rs classify_local_origin, registryhost.rs advertise_local.


60	pub const EVENT_TYPE_SHELL_TEXT: &str = "shell_text";
61	/// A shell file-transfer notice (durable text+file channel): `xfer-id` keys
62	/// the progress-queryable transfer record; `path` is where the blob landed.
63	pub const EVENT_TYPE_SHELL_FILE: &str = "shell_file";
64	/// A shell sensory payload (shell→agent, REST-only — never spooled, dropped
65	/// with a diagnostic unless the owner is live): `sensory-type` carries the
66	/// manifest-declared type.
67	pub const EVENT_TYPE_SENSORY: &str = "sensory";
68	/// A shell pre-close instruction (machinery, fired on link-break — CONTEXT
69	/// §Shell model lifecycle): the body is the manifest's `pre_close` string.
70	/// Not vocabulary-checked — the vocabulary bounds what an *agent* may ask;
71	/// a manifest instructing its own binary is its own authority.
72	pub const EVENT_TYPE_SHELL_CLOSE: &str = "shell_close";
73
74	/// The only event types a sandboxed Psyche may author on its stdout outbound
75	/// channel (ADR-0012): `reply` and `notify`. Everything else in a Psyche's
76	/// stdout — including a `commune` (the Summarizer's inbound delta, never a
77	/// Psyche output) — is not a relayable intent and must be ignored by the
78	/// daemon's parse-and-relay path (KNOWN-HAZARDS 7.3).
79	// [impl->REQ-HAZARD-PSYCHE-OUTBOUND-PROXY]
80	pub fn is_psyche_authorable_type(event_type: &str) -> bool {
81	    event_type == EVENT_TYPE_REPLY || event_type == EVENT_TYPE_NOTIFY
82	}
83
84	// ---------------------------------------------------------------------------
85	// user-msg identity gate (CONTEXT §Gateway — identity-gated, never
86	// payload-trusted; KNOWN-HAZARDS 7.3/7.5)
87	// ---------------------------------------------------------------------------
88
89	/// The open-type wire tag a Gateway endpoint advertises (CONTEXT §Gateway:
90	/// "registered via the open type system, first instance downstream"). The
91	/// `user-msg` identity gate and the Gateway acceptance path both key on this
92	/// tag rather than a hardcoded [`EndpointType`] variant, so the core taxonomy
93	/// stays open.
94	pub const GATEWAY_TAG: &str = "gateway";
95
96	/// The transport-proven origin of a message, for the [`gate_user_msg_type`]
97	/// identity gate. Constructed from **who the sender is** — a bound endpoint's
98	/// resolved type, or a bare local CLI with no agent-endpoint identity behind it
99	/// — never from the message payload. A forged `from` attribute or any
100	/// payload-claimed origin is inert here (KNOWN-HAZARDS 7.3/7.5): authority comes
101	/// from who you are, not what you wrote.
102	#[derive(Debug, Clone, PartialEq, Eq)]
103	pub enum MsgOrigin {
104	    /// The local user's own CLI: a bare `spt send` whose sender resolves to no
105	    /// agent-family endpoint. User-backed by the single-box trust posture (a
106	    /// hosted agent acts through its *endpoint*, the [`MsgOrigin::Endpoint`]
107	    /// arm — not this one).
108	    LocalUserCli,
109	    /// A bound endpoint of a known type — a local emit, or a WAN inbound whose
110	    /// origin endpoint was resolved from the registry keyed by the QUIC-proven
111	    /// remote node id (never wire bytes).
112	    Endpoint(EndpointType),
113	}
114
115	/// Whether an endpoint type is a Gateway (the open-type [`GATEWAY_TAG`]). A
116	/// Gateway rides the open type system rather than the day-one enum, so the
117	/// check is on the tag, not a variant.
118	// [impl->REQ-MSG-5]
119	pub fn is_gateway_endpoint(ty: &EndpointType) -> bool {
120	    matches!(ty, EndpointType::Other(tag) if tag == GATEWAY_TAG)
121	}
122
123	/// Whether `origin` is a **user-backed** origin permitted to author `user-msg`
124	/// (CONTEXT §Gateway). User-backed = the local user's CLI, or a Gateway
125	/// endpoint. Every agent-family endpoint — and every other type (Shell, broker,
126	/// node, foreign) — is not.
127	// [impl->REQ-MSG-5]
128	pub fn is_user_backed_origin(origin: &MsgOrigin) -> bool {
129	    match origin {
130	        MsgOrigin::LocalUserCli => true,
131	        MsgOrigin::Endpoint(ty) => is_gateway_endpoint(ty),
132	    }
133	}
134
135	/// The **effective** event type for a send that requests `user-msg`: honored
136	/// only from a user-backed origin, re-stamped down to plain [`EVENT_TYPE_MSG`]
137	/// for anyone else — degraded, **never rejected** (CONTEXT §Gateway; the daemon
138	/// logs the re-stamp loudly). Authority is the resolved `origin`, never the
139	/// request flag or the payload. A send that does not request `user-msg` is

pub const EVENT_TYPE_SHELL_TEXT: &str = "shell_text";
/// A shell file-transfer notice (durable text+file channel): `xfer-id` keys
/// the progress-queryable transfer record; `path` is where the blob landed.
pub const EVENT_TYPE_SHELL_FILE: &str = "shell_file";
/// A shell sensory payload (shell→agent, REST-only — never spooled, dropped
/// with a diagnostic unless the owner is live): `sensory-type` carries the
/// manifest-declared type.
pub const EVENT_TYPE_SENSORY: &str = "sensory";
/// A shell pre-close instruction (machinery, fired on link-break — CONTEXT
/// §Shell model lifecycle): the body is the manifest's `pre_close` string.
/// Not vocabulary-checked — the vocabulary bounds what an *agent* may ask;
/// a manifest instructing its own binary is its own authority.
pub const EVENT_TYPE_SHELL_CLOSE: &str = "shell_close";

/// The only event types a sandboxed Psyche may author on its stdout outbound
/// channel (ADR-0012): `reply` and `notify`. Everything else in a Psyche's
/// stdout — including a `commune` (the Summarizer's inbound delta, never a
/// Psyche output) — is not a relayable intent and must be ignored by the
/// daemon's parse-and-relay path (KNOWN-HAZARDS 7.3).
// [impl->REQ-HAZARD-PSYCHE-OUTBOUND-PROXY]
pub fn is_psyche_authorable_type(event_type: &str) -> bool {
    event_type == EVENT_TYPE_REPLY || event_type == EVENT_TYPE_NOTIFY
}

// ---------------------------------------------------------------------------
// user-msg identity gate (CONTEXT §Gateway — identity-gated, never
// payload-trusted; KNOWN-HAZARDS 7.3/7.5)
// ---------------------------------------------------------------------------

/// The open-type wire tag a Gateway endpoint advertises (CONTEXT §Gateway:
/// "registered via the open type system, first instance downstream"). The
/// `user-msg` identity gate and the Gateway acceptance path both key on this
/// tag rather than a hardcoded [`EndpointType`] variant, so the core taxonomy
/// stays open.
pub const GATEWAY_TAG: &str = "gateway";

/// The transport-proven origin of a message, for the [`gate_user_msg_type`]
/// identity gate. Constructed from **who the sender is** — a bound endpoint's
/// resolved type, or a bare local CLI with no agent-endpoint identity behind it
/// — never from the message payload. A forged `from` attribute or any
/// payload-claimed origin is inert here (KNOWN-HAZARDS 7.3/7.5): authority comes
/// from who you are, not what you wrote.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MsgOrigin {
    /// The local user's own CLI: a bare `spt send` whose sender resolves to no
    /// agent-family endpoint. User-backed by the single-box trust posture (a
    /// hosted agent acts through its *endpoint*, the [`MsgOrigin::Endpoint`]
    /// arm — not this one).
    LocalUserCli,
    /// A bound endpoint of a known type — a local emit, or a WAN inbound whose
    /// origin endpoint was resolved from the registry keyed by the QUIC-proven
    /// remote node id (never wire bytes).
    Endpoint(EndpointType),
}

/// Whether an endpoint type is a Gateway (the open-type [`GATEWAY_TAG`]). A
/// Gateway rides the open type system rather than the day-one enum, so the
/// check is on the tag, not a variant.
// [impl->REQ-MSG-5]
pub fn is_gateway_endpoint(ty: &EndpointType) -> bool {
    matches!(ty, EndpointType::Other(tag) if tag == GATEWAY_TAG)
}

/// Whether `origin` is a **user-backed** origin permitted to author `user-msg`
/// (CONTEXT §Gateway). User-backed = the local user's CLI, or a Gateway
/// endpoint. Every agent-family endpoint — and every other type (Shell, broker,
/// node, foreign) — is not.
// [impl->REQ-MSG-5]
pub fn is_user_backed_origin(origin: &MsgOrigin) -> bool {
    match origin {
        MsgOrigin::LocalUserCli => true,
        MsgOrigin::Endpoint(ty) => is_gateway_endpoint(ty),
    }
}

/// The **effective** event type for a send that requests `user-msg`: honored
/// only from a user-backed origin, re-stamped down to plain [`EVENT_TYPE_MSG`]
/// for anyone else — degraded, **never rejected** (CONTEXT §Gateway; the daemon
/// logs the re-stamp loudly). Authority is the resolved `origin`, never the
/// request flag or the payload. A send that does not request `user-msg` is


2150	        .duration_since(std::time::UNIX_EPOCH)
2151	        .map(|d| d.as_millis() as u64)
2152	        .unwrap_or(0)
2153	}
2154
2155	/// Resolve the sender id: explicit `--from`, else session auto-detect, else "".
2156	fn resolve_from(from: Option<String>) -> String {
2157	    from.or_else(roster::detect_self_id).unwrap_or_default()
2158	}
2159
2160	/// Classify the LOCAL message origin for the user-msg identity gate
2161	/// (REQ-MSG-5). The subject is the **session-proven** self id (callers pass
2162	/// [`roster::detect_self_id`]), never the `--from` reply label: a hosted agent's
2163	/// session resolves to its endpoint type (read from its perch `info.json`); a
2164	/// bare CLI with no agent-endpoint session is the local user (user-backed). A
2165	/// detected id with no bound perch is treated as the bare user (single-box
2166	/// trust posture) — `--from` alone never confers an agent identity.
2167	fn classify_local_origin(self_id: Option<&str>) -> MsgOrigin {
2168	    match self_id {
2169	        Some(id) if !id.is_empty() => {
2170	            let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
2171	            match spt_store::info::read_info(&perch_path) {
2172	                Some(rec) => MsgOrigin::Endpoint(EndpointType::from_tag(&rec.state)),
2173	                None => MsgOrigin::LocalUserCli,
2174	            }
2175	        }
2176	        _ => MsgOrigin::LocalUserCli,
2177	    }
2178	}
2179
2180	/// Apply the user-msg gate to a local send: returns the body to actually
2181	/// deliver and whether a re-stamp occurred (for the loud log). When honored,
2182	/// the body becomes a `user-msg` typed envelope (riding the verbatim
2183	/// typed-envelope rails); otherwise the plain body rides as `msg`. An empty body
2184	/// is never composed — the downstream empty-refusal must still fire — and is
2185	/// never reported as re-stamped.
2186	// [impl->REQ-MSG-5]
2187	fn apply_user_msg_gate(
2188	    self_id: Option<&str>,
2189	    from: &str,
2190	    body: &str,
2191	    requested: bool,
2192	) -> (String, bool) {
2193	    if !requested || body.trim().is_empty() {
2194	        return (body.to_string(), false);
2195	    }
2196	    let origin = classify_local_origin(self_id);
2197	    if gate_user_msg_type(true, &origin) == EVENT_TYPE_USER_MSG {
2198	        (compose_user_msg_event(from, body), false)
2199	    } else {
2200	        // Requested but the origin is not user-backed → degraded to msg, loud.
2201	        (body.to_string(), true)
2202	    }
2203	}
2204
2205	fn cmd_send(
2206	    target: Option<String>,
2207	    from: Option<String>,
2208	    reply_to: Option<String>,
2209	    deferred: bool,
2210	    user_msg: bool,
2211	) -> i32 {
2212	    let is_reply = reply_to.is_some();
2213	    // --reply-to provides the target when no positional target is given.
2214	    let target = match target.or_else(|| reply_to.clone()) {
2215	        Some(t) => t,
2216	        None => {
2217	            eprintln!("USAGE: a target (or --reply-to <sender>) is required");
2218	            return 2;
2219	        }

        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0)
}

/// Resolve the sender id: explicit `--from`, else session auto-detect, else "".
fn resolve_from(from: Option<String>) -> String {
    from.or_else(roster::detect_self_id).unwrap_or_default()
}

/// Classify the LOCAL message origin for the user-msg identity gate
/// (REQ-MSG-5). The subject is the **session-proven** self id (callers pass
/// [`roster::detect_self_id`]), never the `--from` reply label: a hosted agent's
/// session resolves to its endpoint type (read from its perch `info.json`); a
/// bare CLI with no agent-endpoint session is the local user (user-backed). A
/// detected id with no bound perch is treated as the bare user (single-box
/// trust posture) — `--from` alone never confers an agent identity.
fn classify_local_origin(self_id: Option<&str>) -> MsgOrigin {
    match self_id {
        Some(id) if !id.is_empty() => {
            let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
            match spt_store::info::read_info(&perch_path) {
                Some(rec) => MsgOrigin::Endpoint(EndpointType::from_tag(&rec.state)),
                None => MsgOrigin::LocalUserCli,
            }
        }
        _ => MsgOrigin::LocalUserCli,
    }
}

/// Apply the user-msg gate to a local send: returns the body to actually
/// deliver and whether a re-stamp occurred (for the loud log). When honored,
/// the body becomes a `user-msg` typed envelope (riding the verbatim
/// typed-envelope rails); otherwise the plain body rides as `msg`. An empty body
/// is never composed — the downstream empty-refusal must still fire — and is
/// never reported as re-stamped.
// [impl->REQ-MSG-5]
fn apply_user_msg_gate(
    self_id: Option<&str>,
    from: &str,
    body: &str,
    requested: bool,
) -> (String, bool) {
    if !requested || body.trim().is_empty() {
        return (body.to_string(), false);
    }
    let origin = classify_local_origin(self_id);
    if gate_user_msg_type(true, &origin) == EVENT_TYPE_USER_MSG {
        (compose_user_msg_event(from, body), false)
    } else {
        // Requested but the origin is not user-backed → degraded to msg, loud.
        (body.to_string(), true)
    }
}

fn cmd_send(
    target: Option<String>,
    from: Option<String>,
    reply_to: Option<String>,
    deferred: bool,
    user_msg: bool,
) -> i32 {
    let is_reply = reply_to.is_some();
    // --reply-to provides the target when no positional target is given.
    let target = match target.or_else(|| reply_to.clone()) {
        Some(t) => t,
        None => {
            eprintln!("USAGE: a target (or --reply-to <sender>) is required");
            return 2;
        }


Now read advertise_local in registryhost.rs.


300	                    return RegistryApplyVerdict::UntrustedOrigin;
301	                }
302	                admitted_any = true;
303	                let reg = regs.entry(u.subnet.clone()).or_default();
304	                RegistryApplyVerdict::Merged(reg.merge_node_label(
305	                    &u.node,
306	                    u.label.clone(),
307	                    u.epoch,
308	                ))
309	            })
310	            .collect();
311	        drop(regs);
312	        if admitted_any {
313	            self.heard
314	                .lock()
315	                .unwrap()
316	                .insert(origin_node.to_string(), wall_ms());
317	        }
318	        self.write_snapshots();
319	        verdicts
320	    }
321
322	    /// Scan the owlery and (re-)advertise every self perch into every member
323	    /// subnet it is visible in, each row stamped with a fresh epoch. Returns
324	    /// the updates to push to peers (the node's own rows only — peers' rows
325	    /// reconverge from their own re-advertisement, no transitive gossip in
326	    /// v1: trust is pairwise per subnet, a relayed third-party row would
327	    /// carry an origin the receiver can't gate honestly).
328	    // [impl->REQ-INST-7]
329	    pub fn advertise_local(&self, owlery: &Path) -> Vec<RegistryFeedRecord> {
330	        let subnets = SubnetStore::load();
331	        let mut vis = VisibilityStore::load();
332	        // First-join home adoption (D9-5, REQ-INST-15): within one
333	        // advertisement cadence of the node's first join (a pair lands the
334	        // subnet record; no pairing-protocol threading needed), unhomed Self
335	        // perches adopt the sole subnet + seed their sync scope. Strictly
336	        // adopt-if-unset; a multi-subnet store adopts nothing.
337	        // [impl->REQ-INST-15]
338	        if !spt_store::home::adopt_for_unset(owlery, &subnets, &mut vis).is_empty() {
339	            let _ = vis.save();
340	        }
341	        let vis = vis;
342	        // The node-level resource-blurb seed (D9-4): an endpoint that has not
343	        // authored its own blurb advertises this default (config seeds, the
344	        // agent refines — CONTEXT §resource advertisement).
345	        let node_blurb = crate::config::DaemonConfig::load().resources_blurb;
346	        // The node's subnet-broadcast shell adapters (M5-D8c, CONTEXT
347	        // §Discovery scope): registered kind="shell" adapters that chose
348	        // broadcast=subnet ride every row this node authors — the other-node
349	        // discovery arm. Computed once per scan (the registration set is
350	        // node-level).
351	        // [impl->REQ-EP-5]
352	        let shell_adapters: Vec<String> = spt_runtime::registry::registered(&perch::adapters_dir())
353	            .iter()
354	            .filter(|(r, m)| {
355	                r.kind == spt_runtime::manifest::AdapterKind::Shell
356	                    && m.shell.as_ref().and_then(|s| s.broadcast)
357	                        == Some(spt_runtime::manifest::Broadcast::Subnet)
358	            })
359	            .map(|(r, _)| r.name.clone())
360	            .collect();
361	        let mut out = Vec::new();
362	        let mut regs = self.regs.lock().unwrap();
363	        let mut epoch = self.epoch.lock().unwrap();
364
365	        for id in perch::list_self_perch_ids(owlery) {
366	            let perch_path = perch::resolve_perch_path_in(owlery, &id, ParentHint::Infer);
367	            // Advertisement follows the resting-state machine (D9-2,
368	            // REQ-INST-3): a live perch with a resting record advertises its
369	            // machine state (active / dormant / suspended); a recordless
370	            // (pre-D9-2 / interim) or dead perch keeps the legacy
371	            // liveness-derived mapping. Every advertisement stamps a fresh
372	            // epoch below, so a transition reaches peers as an epoch-bumped
373	            // update — the lease orders it (KH 4.8).
374	            // [impl->REQ-INST-3]
375	            let status = if is_perch_alive(&perch_path) {
376	                match crate::resting::read_rest(&perch_path).map(|r| r.state) {
377	                    Some(crate::resting::RestState::Active) | None => Status::Active,
378	                    Some(crate::resting::RestState::Dormant) => Status::Dormant,
379	                    Some(crate::resting::RestState::Suspended) => Status::Suspended,
380	                }
381	            } else {
382	                Status::Dormant
383	            };
384	            // Endpoint-authored blurb wins; the node seed fills the gap
385	            // (both-authored, REQ-INST-14). Rides the same epoch lease as
386	            // every instance update — a blurb edit reaches peers as an
387	            // ordered, epoch-bumped row.
388	            // [impl->REQ-INST-14]
389	            let info = spt_store::info::read_info(&perch_path);
390	            let resources = info
391	                .as_ref()
392	                .and_then(|i| i.resources.clone())
393	                .or_else(|| node_blurb.clone());
394	            // The presence datum (M5-D6a, REQ-PRES-1): the perch's heartbeat
395	            // stamp (`lifecycle::touch_active` — Active seats only; a resting
396	            // seat never stamps) rides the row like the blurb does — same
397	            // epoch lease, same visibility gate, no new channel. Subnet-wide
398	            // most-recently-active is a projection of these rows (D6b).
399	            // [impl->REQ-PRES-1]
400	            let last_active_ms = info.and_then(|i| i.last_active_ms);
401	            for sub in &subnets.subnets {
402	                let Ok(ep) = epoch.next_epoch() else { continue };
403	                let instance = Instance {
404	                    node: self.node_hex.clone(),
405	                    status,
406	                    epoch: ep,
407	                    resources: resources.clone(),
408	                    last_active_ms,
409	                    shell_adapters: shell_adapters.clone(),
410	                    // The node label rides every row this node authors
411	                    // (REQ-SUBNET-3) — set from the OS hostname at daemon
412	                    // startup, so a rename converges by re-advertisement.
413	                    // [impl->REQ-SUBNET-3]
414	                    node_label: self.node_label.clone(),
415	                    // The hashed machine id rides beside it (M8 decision
416	                    // 13/22) — the re-pair overwrite anchor.
417	                    // [impl->REQ-SUBNET-7]
418	                    machine_id: self.machine_id.clone(),
419	                };
420	                let reg = regs.entry(sub.name.clone()).or_default();
421	                match advertise_if_visible(
422	                    reg,
423	                    &vis,
424	                    &sub.name,
425	                    sub.hide_new_endpoints,
426	                    &id,
427	                    instance.clone(),
428	                ) {
429	                    Ok(Some(_)) => out.push(RegistryFeedRecord::Instance(RegistryUpdate {

                    return RegistryApplyVerdict::UntrustedOrigin;
                }
                admitted_any = true;
                let reg = regs.entry(u.subnet.clone()).or_default();
                RegistryApplyVerdict::Merged(reg.merge_node_label(
                    &u.node,
                    u.label.clone(),
                    u.epoch,
                ))
            })
            .collect();
        drop(regs);
        if admitted_any {
            self.heard
                .lock()
                .unwrap()
                .insert(origin_node.to_string(), wall_ms());
        }
        self.write_snapshots();
        verdicts
    }

    /// Scan the owlery and (re-)advertise every self perch into every member
    /// subnet it is visible in, each row stamped with a fresh epoch. Returns
    /// the updates to push to peers (the node's own rows only — peers' rows
    /// reconverge from their own re-advertisement, no transitive gossip in
    /// v1: trust is pairwise per subnet, a relayed third-party row would
    /// carry an origin the receiver can't gate honestly).
    // [impl->REQ-INST-7]
    pub fn advertise_local(&self, owlery: &Path) -> Vec<RegistryFeedRecord> {
        let subnets = SubnetStore::load();
        let mut vis = VisibilityStore::load();
        // First-join home adoption (D9-5, REQ-INST-15): within one
        // advertisement cadence of the node's first join (a pair lands the
        // subnet record; no pairing-protocol threading needed), unhomed Self
        // perches adopt the sole subnet + seed their sync scope. Strictly
        // adopt-if-unset; a multi-subnet store adopts nothing.
        // [impl->REQ-INST-15]
        if !spt_store::home::adopt_for_unset(owlery, &subnets, &mut vis).is_empty() {
            let _ = vis.save();
        }
        let vis = vis;
        // The node-level resource-blurb seed (D9-4): an endpoint that has not
        // authored its own blurb advertises this default (config seeds, the
        // agent refines — CONTEXT §resource advertisement).
        let node_blurb = crate::config::DaemonConfig::load().resources_blurb;
        // The node's subnet-broadcast shell adapters (M5-D8c, CONTEXT
        // §Discovery scope): registered kind="shell" adapters that chose
        // broadcast=subnet ride every row this node authors — the other-node
        // discovery arm. Computed once per scan (the registration set is
        // node-level).
        // [impl->REQ-EP-5]
        let shell_adapters: Vec<String> = spt_runtime::registry::registered(&perch::adapters_dir())
            .iter()
            .filter(|(r, m)| {
                r.kind == spt_runtime::manifest::AdapterKind::Shell
                    && m.shell.as_ref().and_then(|s| s.broadcast)
                        == Some(spt_runtime::manifest::Broadcast::Subnet)
            })
            .map(|(r, _)| r.name.clone())
            .collect();
        let mut out = Vec::new();
        let mut regs = self.regs.lock().unwrap();
        let mut epoch = self.epoch.lock().unwrap();

        for id in perch::list_self_perch_ids(owlery) {
            let perch_path = perch::resolve_perch_path_in(owlery, &id, ParentHint::Infer);
            // Advertisement follows the resting-state machine (D9-2,
            // REQ-INST-3): a live perch with a resting record advertises its
            // machine state (active / dormant / suspended); a recordless
            // (pre-D9-2 / interim) or dead perch keeps the legacy
            // liveness-derived mapping. Every advertisement stamps a fresh
            // epoch below, so a transition reaches peers as an epoch-bumped
            // update — the lease orders it (KH 4.8).
            // [impl->REQ-INST-3]
            let status = if is_perch_alive(&perch_path) {
                match crate::resting::read_rest(&perch_path).map(|r| r.state) {
                    Some(crate::resting::RestState::Active) | None => Status::Active,
                    Some(crate::resting::RestState::Dormant) => Status::Dormant,
                    Some(crate::resting::RestState::Suspended) => Status::Suspended,
                }
            } else {
                Status::Dormant
            };
            // Endpoint-authored blurb wins; the node seed fills the gap
            // (both-authored, REQ-INST-14). Rides the same epoch lease as
            // every instance update — a blurb edit reaches peers as an
            // ordered, epoch-bumped row.
            // [impl->REQ-INST-14]
            let info = spt_store::info::read_info(&perch_path);
            let resources = info
                .as_ref()
                .and_then(|i| i.resources.clone())
                .or_else(|| node_blurb.clone());
            // The presence datum (M5-D6a, REQ-PRES-1): the perch's heartbeat
            // stamp (`lifecycle::touch_active` — Active seats only; a resting
            // seat never stamps) rides the row like the blurb does — same
            // epoch lease, same visibility gate, no new channel. Subnet-wide
            // most-recently-active is a projection of these rows (D6b).
            // [impl->REQ-PRES-1]
            let last_active_ms = info.and_then(|i| i.last_active_ms);
            for sub in &subnets.subnets {
                let Ok(ep) = epoch.next_epoch() else { continue };
                let instance = Instance {
                    node: self.node_hex.clone(),
                    status,
                    epoch: ep,
                    resources: resources.clone(),
                    last_active_ms,
                    shell_adapters: shell_adapters.clone(),
                    // The node label rides every row this node authors
                    // (REQ-SUBNET-3) — set from the OS hostname at daemon
                    // startup, so a rename converges by re-advertisement.
                    // [impl->REQ-SUBNET-3]
                    node_label: self.node_label.clone(),
                    // The hashed machine id rides beside it (M8 decision
                    // 13/22) — the re-pair overwrite anchor.
                    // [impl->REQ-SUBNET-7]
                    machine_id: self.machine_id.clone(),
                };
                let reg = regs.entry(sub.name.clone()).or_default();
                match advertise_if_visible(
                    reg,
                    &vis,
                    &sub.name,
                    sub.hide_new_endpoints,
                    &id,
                    instance.clone(),
                ) {
                    Ok(Some(_)) => out.push(RegistryFeedRecord::Instance(RegistryUpdate {


Bash

git ls-files "*info*"; echo "---"; grep -rn "pub struct.*Info\|pub state\|pub fn read_info" crates/spt-store/src/info.rs 2>/dev/null | head -20

Find info record struct


crates/spt-store/src/info.rs
crates/spt-store/src/shellinfo.rs
---
38:pub struct InfoJson {
48:    pub state: String,
258:pub fn read_info(perch_path: &Path) -> Option<InfoJson> {


38	pub struct InfoJson {
39	    /// The perch / agent id.
40	    pub id: String,
41	    /// Creation timestamp (opaque string; producer's format).
42	    pub started: String,
43	    /// Owning process pid, or the `"BUSY"` sentinel.
44	    pub pid: PidValue,
45	    /// Harness session id bound to this perch.
46	    pub session_id: String,
47	    /// Endpoint type tag (`spt_proto::endpoint::EndpointType::as_tag`).
48	    pub state: String,
49	    #[serde(skip_serializing_if = "Option::is_none")]
50	    pub parent_pid: Option<u32>,
51	    /// Working directory the perch was created in (forward-slash normalized).
52	    #[serde(skip_serializing_if = "Option::is_none")]
53	    pub cwd: Option<String>,
54	    /// Daemon-managed liveness status (`online`/`offline`) for **daemon-hosted**
55	    /// perches (Psyche, spt-hosted Self). Present ⇒ the daemon owns this perch's
56	    /// liveness and it is read authoritatively, **never** `is_process_alive(pid)`
57	    /// (KNOWN-HAZARDS 2.5). Absent ⇒ an external/interim perch whose liveness is
58	    /// the per-pid probe. The single liveness resolver
59	    /// ([`crate::liveness::is_perch_alive`]) is the only consumer.
60	    #[serde(skip_serializing_if = "Option::is_none")]
61	    pub status: Option<String>,
62	    /// Last-active wall-clock stamp (epoch ms), refreshed by the daemon on this
63	    /// endpoint's activity (pulse tick / input / bind). The recency signal the
64	    /// consent gate resolves "most-recently-active live session" against
65	    /// (REQ-UPD-4) — a v1 precursor to PresenceChannel dispatch. Absent ⇒ never
66	    /// stamped (an interim perch, or one with no activity yet); a resolver treats
67	    /// that as least-recent.
68	    #[serde(skip_serializing_if = "Option::is_none")]
69	    pub last_active_ms: Option<u64>,
70	    /// The node currently remote-driving this endpoint's session (hex node id),
71	    /// stamped by the daemon when it serves a net-attached surface and cleared
72	    /// when the viewport ends (REQ-REACH-1: detection is a daemon fact, not an
73	    /// agent guess). Absent ⇒ locally driven (or never net-attached). `default`
74	    /// so a pre-D5c record (no field) still parses.
75	    #[serde(default, skip_serializing_if = "Option::is_none")]
76	    pub driven_by: Option<String>,
77	    /// The daemon-owned resting state (D9-2, REQ-INST-3): `"active"` /
78	    /// `"dormant"` / `"suspended"` — the explicit instance state machine's
79	    /// durable record (`spt_daemon::resting` owns the transitions; this field
80	    /// is only ever written through [`set_rest_state`]). Distinct from
81	    /// `status`: `status` is daemon **liveness** (online/offline, KH 2.5);
82	    /// `rest_state` is the *resting model* of a live instance. Absent ⇒ a
83	    /// pre-D9-2 / interim record (readers fall back to liveness-derived
84	    /// status). `default` so older records still parse.
85	    #[serde(default, skip_serializing_if = "Option::is_none")]
86	    pub rest_state: Option<String>,
87	    /// When the instance entered dormancy (epoch ms) — the anchor the opt-in
88	    /// `auto-suspend-after` threshold counts from (REQ-INST-3: counted from
89	    /// dormancy onset, never from last activity). Present iff `rest_state`
90	    /// is `"dormant"`; cleared on every other transition.
91	    #[serde(default, skip_serializing_if = "Option::is_none")]
92	    pub dormant_since_ms: Option<u64>,
93	    /// Per-endpoint `auto-suspend-after` override (ms) — the endpoint leg of
94	    /// the global → node → endpoint knob chain (REQ-INST-3). Absent ⇒ inherit
95	    /// the node knob (`daemon.json`); `0` ⇒ explicitly OFF for this endpoint
96	    /// even when the node enables it.
97	    #[serde(default, skip_serializing_if = "Option::is_none")]

pub struct InfoJson {
    /// The perch / agent id.
    pub id: String,
    /// Creation timestamp (opaque string; producer's format).
    pub started: String,
    /// Owning process pid, or the `"BUSY"` sentinel.
    pub pid: PidValue,
    /// Harness session id bound to this perch.
    pub session_id: String,
    /// Endpoint type tag (`spt_proto::endpoint::EndpointType::as_tag`).
    pub state: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub parent_pid: Option<u32>,
    /// Working directory the perch was created in (forward-slash normalized).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cwd: Option<String>,
    /// Daemon-managed liveness status (`online`/`offline`) for **daemon-hosted**
    /// perches (Psyche, spt-hosted Self). Present ⇒ the daemon owns this perch's
    /// liveness and it is read authoritatively, **never** `is_process_alive(pid)`
    /// (KNOWN-HAZARDS 2.5). Absent ⇒ an external/interim perch whose liveness is
    /// the per-pid probe. The single liveness resolver
    /// ([`crate::liveness::is_perch_alive`]) is the only consumer.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status: Option<String>,
    /// Last-active wall-clock stamp (epoch ms), refreshed by the daemon on this
    /// endpoint's activity (pulse tick / input / bind). The recency signal the
    /// consent gate resolves "most-recently-active live session" against
    /// (REQ-UPD-4) — a v1 precursor to PresenceChannel dispatch. Absent ⇒ never
    /// stamped (an interim perch, or one with no activity yet); a resolver treats
    /// that as least-recent.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub last_active_ms: Option<u64>,
    /// The node currently remote-driving this endpoint's session (hex node id),
    /// stamped by the daemon when it serves a net-attached surface and cleared
    /// when the viewport ends (REQ-REACH-1: detection is a daemon fact, not an
    /// agent guess). Absent ⇒ locally driven (or never net-attached). `default`
    /// so a pre-D5c record (no field) still parses.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub driven_by: Option<String>,
    /// The daemon-owned resting state (D9-2, REQ-INST-3): `"active"` /
    /// `"dormant"` / `"suspended"` — the explicit instance state machine's
    /// durable record (`spt_daemon::resting` owns the transitions; this field
    /// is only ever written through [`set_rest_state`]). Distinct from
    /// `status`: `status` is daemon **liveness** (online/offline, KH 2.5);
    /// `rest_state` is the *resting model* of a live instance. Absent ⇒ a
    /// pre-D9-2 / interim record (readers fall back to liveness-derived
    /// status). `default` so older records still parse.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub rest_state: Option<String>,
    /// When the instance entered dormancy (epoch ms) — the anchor the opt-in
    /// `auto-suspend-after` threshold counts from (REQ-INST-3: counted from
    /// dormancy onset, never from last activity). Present iff `rest_state`
    /// is `"dormant"`; cleared on every other transition.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub dormant_since_ms: Option<u64>,
    /// Per-endpoint `auto-suspend-after` override (ms) — the endpoint leg of
    /// the global → node → endpoint knob chain (REQ-INST-3). Absent ⇒ inherit
    /// the node knob (`daemon.json`); `0` ⇒ explicitly OFF for this endpoint
    /// even when the node enables it.
    #[serde(default, skip_serializing_if = "Option::is_none")]


`InfoJson.state` = endpoint type tag. Good. Now find receive_wan callers + how registry is reachable from the funnel.


Grep

receive_wan|fn instances|RegistryHost|fn resolve.*endpoint_type|pub fn instances


crates\spt\src\wansend.rs:6://! [`spt_daemon::registryhost::RegistryHost`] mirror — stale-tolerant by
crates\spt\src\wansend.rs:12://! (`spt-daemon::wan::receive_wan`).
crates\spt\src\wansend.rs:556:    use spt_daemon::registryhost::RegistryHost;
crates\spt\src\wansend.rs:640:            let registry = Arc::new(RegistryHost::new_at(
crates\spt\src\wansend.rs:760:            let registry = Arc::new(RegistryHost::new_at(
crates\spt\src\cli.rs:3498:        spt_daemon::registryhost::RegistryHost::heard_path(&snap_dir),
crates\spt-net\src\net\registry.rs:191:    pub fn instances(&self, endpoint_id: &str) -> &[Instance] {
crates\spt-daemon\tests\dispatch.rs:106:    let registry = Arc::new(spt_daemon::registryhost::RegistryHost::new("b-dispatch"));
crates\spt-daemon\tests\access.rs:21:use spt_daemon::wan::{receive_wan, WanOutcome};
crates\spt-daemon\tests\access.rs:119:                    return receive_wan(&rec, origin, owlery);
crates\spt-daemon\tests\mesh.rs:43:use spt_daemon::registryhost::RegistryHost;
crates\spt-daemon\tests\mesh.rs:140:    registry: Arc<RegistryHost>,
crates\spt-daemon\tests\mesh.rs:195:        let registry = Arc::new(RegistryHost::new_at(
crates\spt-daemon\tests\pump.rs:23:use spt_daemon::registryhost::{RegistryGatePolicy, RegistryHost};
crates\spt-daemon\tests\pump.rs:181:    let b_registry = Arc::new(RegistryHost::new_at(
crates\spt-daemon\tests\pump.rs:275:    let a_registry = Arc::new(RegistryHost::new_at(
crates\spt-daemon\tests\twohost.rs:68:use spt_daemon::registryhost::RegistryHost;
crates\spt-daemon\tests\twohost.rs:302:) -> (Arc<RegistryHost>, Arc<AtomicBool>) {
crates\spt-daemon\tests\twohost.rs:305:    // `RegistryHost::new` uses): the presence fire path + feed-apply surface
crates\spt-daemon\tests\twohost.rs:308:    let registry = Arc::new(RegistryHost::new_at(
crates\spt-daemon\tests\wanmsg.rs:9://!   into the existing local spool path (`receive_wan` → `spool_wan_message_at`)
crates\spt-daemon\tests\wanmsg.rs:27:use spt_daemon::wan::{receive_wan, WanOutcome};
crates\spt-daemon\tests\wanmsg.rs:141:                    outcomes.push(receive_wan(&rec, origin, owlery));
crates\spt-daemon\tests\wanmsg.rs:153:// a claimed `user-msg` arriving through receive_wan from an origin that is NOT
crates\spt-daemon\tests\wanmsg.rs:175:    let outcome = receive_wan(&m, "deadbeefcafef00d", &owlery);
crates\spt-daemon\tests\wanmsg.rs:198:// receive_wan into the atomic op-id+spool insert; a replayed record (same wire
crates\spt-daemon\tests\wanmsg.rs:324:                    assert_eq!(receive_wan(&rec, &remote, &owlery), WanOutcome::Spooled);
crates\spt-daemon\src\brainproc.rs:264:    let registry = Arc::new(crate::registryhost::RegistryHost::new(node_hex));
crates\spt-daemon\src\dispatch.rs:5://! `serve_sync` / `serve_update` / `apply_notif_feed` / `receive_wan` against
crates\spt-daemon\src\dispatch.rs:36://! | untagged + `target`+`op_id` | WAN message feed ([`crate::wan::receive_wan`]) |
crates\spt-daemon\src\dispatch.rs:79:use crate::registryhost::{RegistryGatePolicy, RegistryHost};
crates\spt-daemon\src\dispatch.rs:82:use crate::wan::receive_wan;
crates\spt-daemon\src\dispatch.rs:201:    registry: Arc<RegistryHost>,
crates\spt-daemon\src\dispatch.rs:290:    registry: &RegistryHost,
crates\spt-daemon\src\dispatch.rs:424:/// the daemon's [`RegistryHost`] under the fail-closed gate (member subnet ∧
crates\spt-daemon\src\dispatch.rs:441:    registry: &RegistryHost,
crates\spt-daemon\src\dispatch.rs:614:                    let _ = receive_wan(&msg, origin, &paths.owlery);
crates\spt-daemon\src\dispatch.rs:632:    registry: Arc<RegistryHost>,
crates\spt-daemon\src\lib.rs:85://! broker-owned QUIC stream funnels through [`wan::receive_wan`] — the ADR-0009
crates\spt-daemon\src\lib.rs:236:pub use wan::{receive_wan, WanOutcome};
crates\spt-daemon\src\registryhost.rs:14://! - **local**: [`advertise_local`](RegistryHost::advertise_local) scans the
crates\spt-daemon\src\registryhost.rs:94:pub struct RegistryHost {
crates\spt-daemon\src\registryhost.rs:122:impl RegistryHost {
crates\spt-daemon\src\registryhost.rs:126:    pub fn new(node_hex: &str) -> RegistryHost {
crates\spt-daemon\src\registryhost.rs:127:        RegistryHost::new_at(
crates\spt-daemon\src\registryhost.rs:139:    pub fn new_at(node_hex: &str, epoch: EpochSource, snapshot_dir: PathBuf) -> RegistryHost {
crates\spt-daemon\src\registryhost.rs:140:        RegistryHost {
crates\spt-daemon\src\registryhost.rs:153:    pub fn with_node_label(mut self, label: Option<String>) -> RegistryHost {
crates\spt-daemon\src\registryhost.rs:161:    pub fn with_machine_id(mut self, machine_id: Option<String>) -> RegistryHost {
crates\spt-daemon\src\registryhost.rs:695:    let snap_path = RegistryHost::snapshot_path(snap_dir, subnet);
crates\spt-daemon\src\registryhost.rs:734:    let marker = RegistryHost::repair_evict_path(snap_dir);
crates\spt-daemon\src\registryhost.rs:848:    fn host(dir: &Path) -> RegistryHost {
crates\spt-daemon\src\registryhost.rs:849:        RegistryHost::new_at(
crates\spt-daemon\src\registryhost.rs:868:        let h = RegistryHost::new_at(
crates\spt-daemon\src\registryhost.rs:939:        let h = RegistryHost::new_at(
crates\spt-daemon\src\registryhost.rs:956:        assert!(RegistryHost::snapshot_path(&snap_dir, "home").exists());
crates\spt-daemon\src\registryhost.rs:992:            &std::fs::read_to_string(RegistryHost::snapshot_path(&snap_dir, "home")).unwrap(),
crates\spt-daemon\src\registryhost.rs:1001:        assert!(RegistryHost::repair_evict_path(&snap_dir).exists());
crates\spt-daemon\src\registryhost.rs:1005:            !RegistryHost::repair_evict_path(&snap_dir).exists(),
crates\spt-daemon\src\registryhost.rs:1020:        assert!(!RegistryHost::repair_evict_path(&snap_dir).exists());
crates\spt-daemon\src\registryhost.rs:1027:        let h2 = RegistryHost::new_at(
crates\spt-daemon\src\registryhost.rs:1315:        let snap = RegistryHost::snapshot_path(&dir.path().join("registry"), "home");
crates\spt-daemon\src\registryhost.rs:1439:            let snap = RegistryHost::snapshot_path(&home.join("registry"), "adv");
crates\spt-daemon\src\registryhost.rs:1458:        assert!(!RegistryHost::heard_path(&dir.path().join("registry")).exists());
crates\spt-daemon\src\registryhost.rs:1462:        let heard_file = RegistryHost::heard_path(&dir.path().join("registry"));
crates\spt-daemon\src\registryhost.rs:1499:                &std::fs::read(RegistryHost::heard_path(&dir.path().join("registry"))).unwrap(),
crates\spt-daemon\src\registryhost.rs:1527:        let snap = RegistryHost::snapshot_path(&dir.path().join("registry"), "home");
crates\spt-daemon\src\pump\update.rs:8://! ([`RegistryHost::with_epoch`], the construction-captured Arc per §Frozen-4),
crates\spt-daemon\src\pump\update.rs:24:use crate::registryhost::RegistryHost;
crates\spt-daemon\src\pump\update.rs:37:    registry: Arc<RegistryHost>,
crates\spt-daemon\src\pump\update.rs:61:        registry: Arc<RegistryHost>,
crates\spt-daemon\src\pump\mod.rs:12://! - **registry push** ([`registry`], [`RegistryHost::advertise_local`] → one
crates\spt-daemon\src\pump\mod.rs:76:use crate::registryhost::RegistryHost;
crates\spt-daemon\src\pump\mod.rs:427:    registry: Arc<RegistryHost>,
crates\spt-daemon\src\pump\mod.rs:844:    registry: Arc<RegistryHost>,
crates\spt-daemon\src\pump\registry.rs:10://! [`RegistryHost`], its `PathBuf` slices out of [`super::PumpPaths`], and the
crates\spt-daemon\src\pump\registry.rs:21:use crate::registryhost::RegistryHost;
crates\spt-daemon\src\pump\registry.rs:64:/// ([`RegistryHost::advertise_local`] → one feed stream per peer). The node
crates\spt-daemon\src\pump\registry.rs:70:    registry: Arc<RegistryHost>,
crates\spt-daemon\src\pump\registry.rs:91:        registry: Arc<RegistryHost>,
crates\spt-daemon\src\pump\registry.rs:223:            registry: Arc::new(RegistryHost::new_at(
crates\spt-daemon\src\pump\registry.rs:282:                registry: Arc::new(RegistryHost::new_at(
crates\spt-daemon\src\pump\sync.rs:21:use crate::registryhost::RegistryHost;
crates\spt-daemon\src\pump\sync.rs:30:    registry: Arc<RegistryHost>,
crates\spt-daemon\src\pump\sync.rs:49:        registry: Arc<RegistryHost>,
crates\spt-daemon\src\wan.rs:101:pub fn receive_wan(msg: &WanMessage, origin_node: &str, owlery: &Path) -> WanOutcome {


Read RegistryHost struct + new methods area, and dispatch.rs receive_wan call site.


94	pub struct RegistryHost {
95	    /// This node's stable name (lowercase-hex pubkey).
96	    node_hex: String,
97	    /// This node's human label (M7 D2, REQ-SUBNET-3): the OS hostname,
98	    /// re-read at daemon startup ([`new`](Self::new)) and stamped onto every
99	    /// advertised row — a hostname change reaches peers as ordinary
100	    /// epoch-bumped updates. Display/addressing sugar; `node_hex` stays the
101	    /// identity.
102	    node_label: Option<String>,
103	    /// This node's hashed stable machine identifier (M8 decision 13/22,
104	    /// REQ-SUBNET-7): stamped onto every advertised row beside the label —
105	    /// the re-pair overwrite anchor. Already domain-separated-hashed by
106	    /// [`crate::machineid`]; never the raw id.
107	    machine_id: Option<String>,
108	    /// When each peer node was last *heard* (an admitted inbound feed,
109	    /// wall-clock ms) — the gossip-recency half of the M7 D2 hybrid
110	    /// liveness (REQ-SUBNET-1): a recently-heard node renders online without
111	    /// a probe; only stale rows get dialed. Local-only (never on the wire);
112	    /// mirrored to `heard.meta` beside the snapshots for the CLI reader.
113	    heard: Mutex<BTreeMap<String, u64>>,
114	    regs: Mutex<BTreeMap<String, SubnetRegistry>>,
115	    /// The node's one durable monotonic counter (the D3b lease source) — the
116	    /// epoch on every locally-authored instance row.
117	    epoch: Mutex<EpochSource>,
118	    /// Where snapshots land (`identity/registry/<subnet>.json`).
119	    snapshot_dir: PathBuf,
120	}
121
122	impl RegistryHost {
123	    /// Stand the host up for this node, with the canonical epoch counter and
124	    /// snapshot dir under the spt home. Reads the OS hostname here — daemon
125	    /// startup IS the label's re-check point (REQ-SUBNET-3).
126	    pub fn new(node_hex: &str) -> RegistryHost {
127	        RegistryHost::new_at(
128	            node_hex,
129	            EpochSource::load(),
130	            perch::identity_dir().join("registry"),
131	        )
132	        .with_node_label(spt_store::hostlabel::os_hostname())
133	        .with_machine_id(crate::machineid::machine_id_hash())
134	    }
135
136	    /// Explicit-paths constructor (tests; the pump E2E gives each in-process
137	    /// "node" its own counter + snapshot dir). No label by default — tests
138	    /// stay hostname-independent; chain [`with_node_label`](Self::with_node_label).
139	    pub fn new_at(node_hex: &str, epoch: EpochSource, snapshot_dir: PathBuf) -> RegistryHost {
140	        RegistryHost {
141	            node_hex: node_hex.to_string(),
142	            node_label: None,
143	            machine_id: None,
144	            heard: Mutex::new(BTreeMap::new()),
145	            regs: Mutex::new(BTreeMap::new()),
146	            epoch: Mutex::new(epoch),
147	            snapshot_dir,
148	        }
149	    }
150
151	    /// Set the label this host stamps onto every advertised row.
152	    // [impl->REQ-SUBNET-3]
153	    pub fn with_node_label(mut self, label: Option<String>) -> RegistryHost {
154	        self.node_label = label;
155	        self
156	    }
157
158	    /// Set the hashed machine identifier this host stamps onto every
159	    /// advertised row (M8 decision 13/22, REQ-SUBNET-7).
160	    // [impl->REQ-SUBNET-7]
161	    pub fn with_machine_id(mut self, machine_id: Option<String>) -> RegistryHost {
162	        self.machine_id = machine_id;
163	        self
164	    }
165
166	    /// This node's stable name.
167	    pub fn node_hex(&self) -> &str {
168	        &self.node_hex
169	    }
170
171	    /// Run `f` with this host's **canonical per-node epoch** — the ONE
172	    /// monotonic counter (identity/epoch.json) behind every locally-authored
173	    /// registry row. Every other per-node-id producer on this node MUST mint
174	    /// from the same source, or `node:epoch`-shaped ids collide: a notif_id is
175	    /// `node_hex:epoch`, so the pump's consent-notif producer and the CLI
176	    /// notify front door (which loads this same canonical source) would
177	    /// otherwise issue the same id from independent counters. (Field bug: the
178	    /// pump minted consent notif_ids from the stream-open op counter
179	    /// `pump-ops.json` — a different file — so on a node whose two counters
180	    /// were both low the consent notif and a presence/notify notif collided on
181	    /// `node:1`. The rig's presence-redirect rung caught it: `UNIQUE
182	    /// constraint failed: notifs.notif_id`.) Locks the shared cursor so the
183	    /// dispatcher's registry arm and the pump never double-mint.
184	    // [impl->REQ-HAZARD-REGISTRY-EPOCH-LEASE]
185	    pub fn with_epoch<R>(&self, f: impl FnOnce(&mut EpochSource) -> R) -> R {
186	        f(&mut self.epoch.lock().unwrap())
187	    }
188
189	    /// Apply one inbound feed under the gate. `origin_node` MUST be the
190	    /// handshake-proven stream-table origin, never payload bytes.
191	    // [impl->REQ-INST-7]
192	    // [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
193	    pub fn apply_feed(
194	        &self,
195	        origin_node: &str,
196	        updates: &[RegistryUpdate],
197	        policy: &RegistryGatePolicy,
198	    ) -> Vec<RegistryApplyVerdict> {
199	        self.apply_feed_flips(origin_node, updates, policy).0
200	    }
201
202	    /// [`apply_feed`](Self::apply_feed) that ALSO reports **attention
203	    /// flips** (D9-2, REQ-INST-3): the endpoint ids whose admitted update was
204	    /// an observed *transition to `Active` on a peer node* — the wire signal
205	    /// that another instance of the id just became the driven one (its
206	    /// daemon advertises transitions epoch-bumped), which rests a local
207	    /// instance of the same id warm (CONTEXT: driving `ling@laptop` makes
208	    /// `ling@desktop` dormant).
209	    ///
210	    /// Flip = `Updated` merge where the stored row's status was NOT `Active`
211	    /// and the incoming one is. Deliberately **never `Inserted`**: this
212	    /// registry is in-memory, so a daemon restart re-inserts every peer row
213	    /// at its *current* status — counting inserts would knock local
214	    /// instances dormant on every restart; and never the periodic
215	    /// re-advertisement (`Updated` with prev already `Active` is not a
216	    /// flip). The lease has already dropped stale/equal epochs before any
217	    /// of this runs.
218	    // [impl->REQ-INST-3]
219	    pub fn apply_feed_flips(
220	        &self,
221	        origin_node: &str,
222	        updates: &[RegistryUpdate],
223	        policy: &RegistryGatePolicy,
224	    ) -> (Vec<RegistryApplyVerdict>, Vec<String>) {
225	        let mut regs = self.regs.lock().unwrap();
226	        let mut flips: Vec<String> = Vec::new();
227	        let mut admitted_any = false;
228	        let verdicts = updates
229	            .iter()
230	            .map(|u| {
231	                if !policy.subnets.iter().any(|s| s == &u.subnet) {
232	                    return RegistryApplyVerdict::NotMember;
233	                }
234	                // [impl->REQ-MESH-5]
235	                if !policy.roster.is_member(&u.subnet, origin_node) {
236	                    return RegistryApplyVerdict::UntrustedOrigin;
237	                }
238	                admitted_any = true;
239	                let reg = regs.entry(u.subnet.clone()).or_default();
240	                let prev = reg
241	                    .instances(&u.endpoint_id)
242	                    .iter()
243	                    .find(|i| i.node == u.instance.node)
244	                    .map(|i| i.status);
245	                // The epoch lease (4.8) decides; an own-row echo from a peer
246	                // is dropped Stale by construction (our epoch is newer or
247	                // equal — never authored elsewhere).
248	                let outcome = reg.merge_instance(&u.endpoint_id, u.instance.clone());
249	                if outcome == MergeOutcome::Updated
250	                    && u.instance.status == Status::Active
251	                    && prev != Some(Status::Active)
252	                    && u.instance.node != self.node_hex
253	                    && !flips.contains(&u.endpoint_id)
254	                {
255	                    flips.push(u.endpoint_id.clone());
256	                }
257	                RegistryApplyVerdict::Merged(outcome)
258	            })
259	            .collect();
260	        drop(regs);
261	        // Gossip-recency stamp (M7 D2): an ADMITTED feed proves the origin
262	        // node is up right now — even a Stale merge verdict is a liveness
263	        // signal (the lease judged the row, not the sender). Refused feeds
264	        // stamp nothing (an untrusted sender earns no liveness credit).
265	        // [impl->REQ-SUBNET-1]
266	        if admitted_any {
267	            self.heard
268	                .lock()

pub struct RegistryHost {
    /// This node's stable name (lowercase-hex pubkey).
    node_hex: String,
    /// This node's human label (M7 D2, REQ-SUBNET-3): the OS hostname,
    /// re-read at daemon startup ([`new`](Self::new)) and stamped onto every
    /// advertised row — a hostname change reaches peers as ordinary
    /// epoch-bumped updates. Display/addressing sugar; `node_hex` stays the
    /// identity.
    node_label: Option<String>,
    /// This node's hashed stable machine identifier (M8 decision 13/22,
    /// REQ-SUBNET-7): stamped onto every advertised row beside the label —
    /// the re-pair overwrite anchor. Already domain-separated-hashed by
    /// [`crate::machineid`]; never the raw id.
    machine_id: Option<String>,
    /// When each peer node was last *heard* (an admitted inbound feed,
    /// wall-clock ms) — the gossip-recency half of the M7 D2 hybrid
    /// liveness (REQ-SUBNET-1): a recently-heard node renders online without
    /// a probe; only stale rows get dialed. Local-only (never on the wire);
    /// mirrored to `heard.meta` beside the snapshots for the CLI reader.
    heard: Mutex<BTreeMap<String, u64>>,
    regs: Mutex<BTreeMap<String, SubnetRegistry>>,
    /// The node's one durable monotonic counter (the D3b lease source) — the
    /// epoch on every locally-authored instance row.
    epoch: Mutex<EpochSource>,
    /// Where snapshots land (`identity/registry/<subnet>.json`).
    snapshot_dir: PathBuf,
}

impl RegistryHost {
    /// Stand the host up for this node, with the canonical epoch counter and
    /// snapshot dir under the spt home. Reads the OS hostname here — daemon
    /// startup IS the label's re-check point (REQ-SUBNET-3).
    pub fn new(node_hex: &str) -> RegistryHost {
        RegistryHost::new_at(
            node_hex,
            EpochSource::load(),
            perch::identity_dir().join("registry"),
        )
        .with_node_label(spt_store::hostlabel::os_hostname())
        .with_machine_id(crate::machineid::machine_id_hash())
    }

    /// Explicit-paths constructor (tests; the pump E2E gives each in-process
    /// "node" its own counter + snapshot dir). No label by default — tests
    /// stay hostname-independent; chain [`with_node_label`](Self::with_node_label).
    pub fn new_at(node_hex: &str, epoch: EpochSource, snapshot_dir: PathBuf) -> RegistryHost {
        RegistryHost {
            node_hex: node_hex.to_string(),
            node_label: None,
            machine_id: None,
            heard: Mutex::new(BTreeMap::new()),
            regs: Mutex::new(BTreeMap::new()),
            epoch: Mutex::new(epoch),
            snapshot_dir,
        }
    }

    /// Set the label this host stamps onto every advertised row.
    // [impl->REQ-SUBNET-3]
    pub fn with_node_label(mut self, label: Option<String>) -> RegistryHost {
        self.node_label = label;
        self
    }

    /// Set the hashed machine identifier this host stamps onto every
    /// advertised row (M8 decision 13/22, REQ-SUBNET-7).
    // [impl->REQ-SUBNET-7]
    pub fn with_machine_id(mut self, machine_id: Option<String>) -> RegistryHost {
        self.machine_id = machine_id;
        self
    }

    /// This node's stable name.
    pub fn node_hex(&self) -> &str {
        &self.node_hex
    }

    /// Run `f` with this host's **canonical per-node epoch** — the ONE
    /// monotonic counter (identity/epoch.json) behind every locally-authored
    /// registry row. Every other per-node-id producer on this node MUST mint
    /// from the same source, or `node:epoch`-shaped ids collide: a notif_id is
    /// `node_hex:epoch`, so the pump's consent-notif producer and the CLI
    /// notify front door (which loads this same canonical source) would
    /// otherwise issue the same id from independent counters. (Field bug: the
    /// pump minted consent notif_ids from the stream-open op counter
    /// `pump-ops.json` — a different file — so on a node whose two counters
    /// were both low the consent notif and a presence/notify notif collided on
    /// `node:1`. The rig's presence-redirect rung caught it: `UNIQUE
    /// constraint failed: notifs.notif_id`.) Locks the shared cursor so the
    /// dispatcher's registry arm and the pump never double-mint.
    // [impl->REQ-HAZARD-REGISTRY-EPOCH-LEASE]
    pub fn with_epoch<R>(&self, f: impl FnOnce(&mut EpochSource) -> R) -> R {
        f(&mut self.epoch.lock().unwrap())
    }

    /// Apply one inbound feed under the gate. `origin_node` MUST be the
    /// handshake-proven stream-table origin, never payload bytes.
    // [impl->REQ-INST-7]
    // [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
    pub fn apply_feed(
        &self,
        origin_node: &str,
        updates: &[RegistryUpdate],
        policy: &RegistryGatePolicy,
    ) -> Vec<RegistryApplyVerdict> {
        self.apply_feed_flips(origin_node, updates, policy).0
    }

    /// [`apply_feed`](Self::apply_feed) that ALSO reports **attention
    /// flips** (D9-2, REQ-INST-3): the endpoint ids whose admitted update was
    /// an observed *transition to `Active` on a peer node* — the wire signal
    /// that another instance of the id just became the driven one (its
    /// daemon advertises transitions epoch-bumped), which rests a local
    /// instance of the same id warm (CONTEXT: driving `ling@laptop` makes
    /// `ling@desktop` dormant).
    ///
    /// Flip = `Updated` merge where the stored row's status was NOT `Active`
    /// and the incoming one is. Deliberately **never `Inserted`**: this
    /// registry is in-memory, so a daemon restart re-inserts every peer row
    /// at its *current* status — counting inserts would knock local
    /// instances dormant on every restart; and never the periodic
    /// re-advertisement (`Updated` with prev already `Active` is not a
    /// flip). The lease has already dropped stale/equal epochs before any
    /// of this runs.
    // [impl->REQ-INST-3]
    pub fn apply_feed_flips(
        &self,
        origin_node: &str,
        updates: &[RegistryUpdate],
        policy: &RegistryGatePolicy,
    ) -> (Vec<RegistryApplyVerdict>, Vec<String>) {
        let mut regs = self.regs.lock().unwrap();
        let mut flips: Vec<String> = Vec::new();
        let mut admitted_any = false;
        let verdicts = updates
            .iter()
            .map(|u| {
                if !policy.subnets.iter().any(|s| s == &u.subnet) {
                    return RegistryApplyVerdict::NotMember;
                }
                // [impl->REQ-MESH-5]
                if !policy.roster.is_member(&u.subnet, origin_node) {
                    return RegistryApplyVerdict::UntrustedOrigin;
                }
                admitted_any = true;
                let reg = regs.entry(u.subnet.clone()).or_default();
                let prev = reg
                    .instances(&u.endpoint_id)
                    .iter()
                    .find(|i| i.node == u.instance.node)
                    .map(|i| i.status);
                // The epoch lease (4.8) decides; an own-row echo from a peer
                // is dropped Stale by construction (our epoch is newer or
                // equal — never authored elsewhere).
                let outcome = reg.merge_instance(&u.endpoint_id, u.instance.clone());
                if outcome == MergeOutcome::Updated
                    && u.instance.status == Status::Active
                    && prev != Some(Status::Active)
                    && u.instance.node != self.node_hex
                    && !flips.contains(&u.endpoint_id)
                {
                    flips.push(u.endpoint_id.clone());
                }
                RegistryApplyVerdict::Merged(outcome)
            })
            .collect();
        drop(regs);
        // Gossip-recency stamp (M7 D2): an ADMITTED feed proves the origin
        // node is up right now — even a Stale merge verdict is a liveness
        // signal (the lease judged the row, not the sender). Refused feeds
        // stamp nothing (an untrusted sender earns no liveness credit).
        // [impl->REQ-SUBNET-1]
        if admitted_any {
            self.heard
                .lock()


595	fn serve_wan_feed(
596	    brain: &mut Brain,
597	    stream_id: u64,
598	    origin: &str,
599	    paths: &DispatchPaths,
600	) -> DispatchOutcome {
601	    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {
602	        return DispatchOutcome::Failed(e.to_string());
603	    }
604	    let mut decoder = WanDecoder::new();
605	    let mut handled = 0usize;
606	    loop {
607	        match brain.read_event() {
608	            Ok(BrokerEvent::NetStreamData {
609	                stream_id: sid,
610	                bytes,
611	                ..
612	            }) if sid == stream_id => {
613	                for msg in decoder.push(&bytes) {
614	                    let _ = receive_wan(&msg, origin, &paths.owlery);
615	                    handled += 1;
616	                }
617	            }
618	            Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {
619	                return DispatchOutcome::Served(format!("wan feed: {handled} record(s)"));
620	            }
621	            Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),
622	            Ok(_) => {}
623	            Err(e) => return DispatchOutcome::Failed(e.to_string()),
624	        }
625	    }
626	}
627
628	/// Spawn the dispatcher on its own thread against the canonical broker
629	/// socket — the daemon-boot entry ([`crate::daemon::Daemon::run`]).
630	pub fn spawn_dispatcher(
631	    broker_name: String,
632	    registry: Arc<RegistryHost>,
633	    stop: Arc<AtomicBool>,
634	) -> thread::JoinHandle<()> {
635	    thread::spawn(move || {
636	        let paths = DispatchPaths::production();
637	        if let Err(e) =
638	            run_dispatch_loop(&broker_name, &paths, registry, &stop, DEFAULT_DISPATCH_POLL)
639	        {

fn serve_wan_feed(
    brain: &mut Brain,
    stream_id: u64,
    origin: &str,
    paths: &DispatchPaths,
) -> DispatchOutcome {
    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {
        return DispatchOutcome::Failed(e.to_string());
    }
    let mut decoder = WanDecoder::new();
    let mut handled = 0usize;
    loop {
        match brain.read_event() {
            Ok(BrokerEvent::NetStreamData {
                stream_id: sid,
                bytes,
                ..
            }) if sid == stream_id => {
                for msg in decoder.push(&bytes) {
                    let _ = receive_wan(&msg, origin, &paths.owlery);
                    handled += 1;
                }
            }
            Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {
                return DispatchOutcome::Served(format!("wan feed: {handled} record(s)"));
            }
            Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),
            Ok(_) => {}
            Err(e) => return DispatchOutcome::Failed(e.to_string()),
        }
    }
}

/// Spawn the dispatcher on its own thread against the canonical broker
/// socket — the daemon-boot entry ([`crate::daemon::Daemon::run`]).
pub fn spawn_dispatcher(
    broker_name: String,
    registry: Arc<RegistryHost>,
    stop: Arc<AtomicBool>,
) -> thread::JoinHandle<()> {
    thread::spawn(move || {
        let paths = DispatchPaths::production();
        if let Err(e) =
            run_dispatch_loop(&broker_name, &paths, registry, &stop, DEFAULT_DISPATCH_POLL)
        {


serve_wan_feed|fn run_dispatch_loop|fn dispatch_one|registry: &?Arc?<?RegistryHost


crates\spt-daemon\src\dispatch.rs:198:pub fn run_dispatch_loop(
crates\spt-daemon\src\dispatch.rs:201:    registry: Arc<RegistryHost>,
crates\spt-daemon\src\dispatch.rs:333:        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths),
crates\spt-daemon\src\dispatch.rs:595:fn serve_wan_feed(
crates\spt-daemon\src\dispatch.rs:632:    registry: Arc<RegistryHost>,


280	    }
281	}
282
283	/// One claimed stream's worker: peek → classify → run the family's serve
284	/// loop on a fresh brain connection.
285	fn worker(
286	    broker_name: &str,
287	    stream_id: u64,
288	    origin: &str,
289	    paths: &DispatchPaths,
290	    registry: &RegistryHost,
291	) -> DispatchOutcome {
292	    let line = match peek_first_line(broker_name, stream_id) {
293	        Ok(Some(v)) => v,
294	        Ok(None) => return DispatchOutcome::Unknown,
295	        Err(e) => return DispatchOutcome::Failed(e.to_string()),
296	    };
297	    let family = classify_first_line(&line);
298
299	    let mut brain = match connect(broker_name) {
300	        Ok(b) => b,
301	        Err(e) => return DispatchOutcome::Failed(e.to_string()),
302	    };
303
304	    match family {
305	        StreamFamily::Sync => {
306	            let cs = match ContextStore::open_or_init() {
307	                Ok(cs) => cs,
308	                Err(e) => return DispatchOutcome::Failed(e.to_string()),
309	            };
310	            let policy = SyncPolicy::load();
311	            match serve_sync(
312	                &mut brain,
313	                stream_id,
314	                origin,
315	                0,
316	                &cs,
317	                &policy,
318	                &paths.scratch,
319	            ) {
320	                Ok(out) => DispatchOutcome::Served(format!("{out:?}")),
321	                Err(e) => DispatchOutcome::Failed(e.to_string()),
322	            }
323	        }
324	        StreamFamily::Update => {
325	            let cache = ReleaseCache::open(&paths.releases);
326	            let roster = RosterStore::load();
327	            match serve_update(&mut brain, stream_id, origin, 0, &cache, &roster) {
328	                Ok(out) => DispatchOutcome::Served(format!("{out:?}")),
329	                Err(e) => DispatchOutcome::Failed(e.to_string()),
330	            }
331	        }
332	        StreamFamily::Notif => serve_notif_feed(&mut brain, stream_id, origin),
333	        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths),
334	        StreamFamily::Registry => serve_registry_feed(&mut brain, stream_id, origin, registry),

    }
}

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

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

    match family {
        StreamFamily::Sync => {
            let cs = match ContextStore::open_or_init() {
                Ok(cs) => cs,
                Err(e) => return DispatchOutcome::Failed(e.to_string()),
            };
            let policy = SyncPolicy::load();
            match serve_sync(
                &mut brain,
                stream_id,
                origin,
                0,
                &cs,
                &policy,
                &paths.scratch,
            ) {
                Ok(out) => DispatchOutcome::Served(format!("{out:?}")),
                Err(e) => DispatchOutcome::Failed(e.to_string()),
            }
        }
        StreamFamily::Update => {
            let cache = ReleaseCache::open(&paths.releases);
            let roster = RosterStore::load();
            match serve_update(&mut brain, stream_id, origin, 0, &cache, &roster) {
                Ok(out) => DispatchOutcome::Served(format!("{out:?}")),
                Err(e) => DispatchOutcome::Failed(e.to_string()),
            }
        }
        StreamFamily::Notif => serve_notif_feed(&mut brain, stream_id, origin),
        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths),
        StreamFamily::Registry => serve_registry_feed(&mut brain, stream_id, origin, registry),


registry available in `worker`. Now check `EndpointType::from_tag` location + remaining registry.rs Instance construction sites (lines 1222+).


pub fn from_tag|pub enum EndpointType|pub fn as_tag


crates\spt-proto\src\endpoint.rs:49:pub enum EndpointType {
crates\spt-proto\src\endpoint.rs:78:    pub fn as_tag(&self) -> String {
crates\spt-proto\src\endpoint.rs:95:    pub fn from_tag(tag: &str) -> EndpointType {
crates\spt-daemon\src\resting.rs:69:    pub fn as_tag(self) -> &'static str {
crates\spt-daemon\src\resting.rs:79:    pub fn from_tag(tag: &str) -> Option<RestState> {


Bash

grep -rn "Instance {" crates/spt-net crates/spt-daemon --include=*.rs | grep -v "JoinCollision\|RegistryUpdate"

All Instance literal construction sites


crates/spt-net/src/net/registry.rs:72:pub struct Instance {
crates/spt-net/src/net/registry.rs:746:    fn inst(node: &str, status: Status, epoch: u64) -> Instance {
crates/spt-net/src/net/registry.rs:747:        Instance {
crates/spt-net/src/net/registry.rs:902:        let with = |blurb: Option<&str>, epoch: u64| Instance {
crates/spt-net/src/net/registry.rs:949:        let with = |label: Option<&str>, epoch: u64| Instance {
crates/spt-net/src/net/registry.rs:1055:        let labeled = |node: &str, label: &str| Instance {
crates/spt-net/src/net/registry.rs:1120:        let with = |ts: Option<u64>, epoch: u64| Instance {
crates/spt-net/src/net/registry.rs:1162:            Instance {
crates/spt-net/src/net/replicate.rs:152:            instance: Instance {
crates/spt-net/src/net/wanmsg.rs:106:            instance: Instance {
crates/spt-daemon/src/notif.rs:673:                spt_net::net::registry::Instance {
crates/spt-daemon/src/presence.rs:217:    fn row(node: &str, status: Status, ts: Option<u64>) -> Instance {
crates/spt-daemon/src/presence.rs:218:        Instance {
crates/spt-daemon/src/reconcile.rs:49:    fn inst(node: &str, status: Status) -> Instance {
crates/spt-daemon/src/reconcile.rs:50:        Instance {
crates/spt-daemon/src/registryhost.rs:403:                let instance = Instance {
crates/spt-daemon/src/registryhost.rs:812:            instance: Instance {
crates/spt-daemon/src/registryhost.rs:892:    fn identified(node: &str, label: &str, mid: &str, epoch: u64) -> Instance {
crates/spt-daemon/src/registryhost.rs:893:        Instance {
crates/spt-daemon/src/registryhost.rs:1203:                instance: Instance {
crates/spt-daemon/src/shelldisc.rs:322:        let inst = |node: &str, status: Status, shells: &[&str]| Instance {
crates/spt-daemon/tests/dispatch.rs:597:        spt_net::net::registry::Instance {
crates/spt-daemon/tests/dispatch.rs:913:        instance: Instance {
crates/spt-daemon/tests/pump.rs:302:                instance: Instance {
crates/spt-daemon/tests/replicate.rs:115:    let active = Instance {
crates/spt-daemon/tests/replicate.rs:147:        instance: Instance {
crates/spt-daemon/tests/sync.rs:425:        Instance {
crates/spt-daemon/tests/sync.rs:435:        Instance {


24 construction sites need `endpoint_type: None` (mostly test helpers). Mechanical. Let me set up task tracking, then execute T1.


ToolSearch

Question: select:TaskCreate,TaskUpdate


Question: select:TaskCreate,TaskUpdate


TaskCreate

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.


Task #1 created successfully: REQ survey + mint REQ-MSG-6

REQ survey + mint REQ-MSG-6


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.


Task #2 created successfully: T1 Instance.endpoint_type field

T1 Instance.endpoint_type field


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.


Task #3 created successfully: T2 populate on advertise_local

T2 populate on advertise_local


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.


Task #4 created successfully: T3 resolve at receive_wan funnel

T3 resolve at receive_wan funnel


T4 unit test matrix

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


Task #5 created successfully: T4 unit test matrix

T4 unit test matrix


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.


Task #6 created successfully: Sweep + traceable + push

Sweep + traceable + push


Start T1 (REQ survey first). Read traceable-reqs.toml relevant reqs.


Bash

grep -n "REQ-MSG-\|REQ-EP-6\|REQ-INST-7" traceable-reqs.toml

Locate relevant reqs in toml


174:id = "REQ-EP-6"
175:title = "Gateway type acceptance: a Gateway-typed perch binds (api bind --type, open type system — un-hardcode the live_agent default), advertises/addressable like any endpoint, owns shells (owner validation not agent-family-gated), subscribes to digests, and is the user-msg identity gate's user-backed origin (REQ-MSG-5); in-tree mock-gateway fixture (R-DOCS-2 pattern, no downstream adapter code). Cross-node WAN Gateway-origin (registry endpoint_type trust) = documented fail-closed residual, deferred past M9 (doyle G2 ruling 2026-06-13)"
186:required_stages = []  # rule 5: the data-model split shipped across M4 D3 (registry Instance rows) + D9-5 (home/adapter on the endpoint record); this umbrella req activates when instantiate-anywhere makes the split user-facing (instance minting) — which DEFERS PAST M5 (user decision 2026-06-04, M5-PLAN scope decision 2: consent framework seam only); its concrete slices are tracked by REQ-INST-7..15
208:id = "REQ-INST-7"
258:id = "REQ-MSG-1"
262:id = "REQ-MSG-2"
266:id = "REQ-MSG-3"
270:id = "REQ-MSG-4"
274:id = "REQ-MSG-5"


172	required_stages = ["impl", "unit", "int"]  # activated M5-D3a; int activated M5-D9b: the D3e shell E2E + the real-shell E2E (notify_shell_e2e.rs, CI-hook-driven against the standalone spt-shell-notify adapter) carry the int tags
173	[[requirements]]
174	id = "REQ-EP-6"
175	title = "Gateway type acceptance: a Gateway-typed perch binds (api bind --type, open type system — un-hardcode the live_agent default), advertises/addressable like any endpoint, owns shells (owner validation not agent-family-gated), subscribes to digests, and is the user-msg identity gate's user-backed origin (REQ-MSG-5); in-tree mock-gateway fixture (R-DOCS-2 pattern, no downstream adapter code). Cross-node WAN Gateway-origin (registry endpoint_type trust) = documented fail-closed residual, deferred past M9 (doyle G2 ruling 2026-06-13)"
176	required_stages = ["doc", "impl", "unit"]  # activated 2026-06-13: M9-T5 (CONTEXT.md §Gateway; api bind --type un-hardcode at establish_perch; gateway-owns-shell + local user-backed-origin E2E via mock-gateway fixture; the WAN fail-closed row tested at the receive_wan funnel)
177	[[requirements]]
178	id = "REQ-EP-7"
179	title = "Durable live-role.md: a per-agent broad-purpose statement in tracked/agents/<id>/ beside live-context.md (replicates with the mind on the same a-<id> branch); renders FIRST at start-transition context injection (role -> live-context -> project-context); SOLE writer `spt endpoint role --overwrite <file>` — mechanical no-automated-writer guarantee (echo-commune ingest / signoff / Psyche reconcile structurally exclude it). The user-backed-origin hard gate on the writer is a deferred later tightening (rides the user-msg identity plumbing)"
180	required_stages = ["doc", "impl", "unit"]  # activated 2026-06-13: M9-T6 (CONTEXT.md §live role; contextstore live_role_file/live_role_path seam; resume::download_psyche_context role-first injection order; spt endpoint role print/--overwrite sole writer; injection-order + no-automated-writer tests; M9-WAVE2-ROLE-WRITERS.md survey)
181
182	# ───────────────────────────── Instances ────────────────────────────────────
183	[[requirements]]
184	id = "REQ-INST-1"
185	title = "endpoint ID vs instance split (adapter-agnostic ID)"
186	required_stages = []  # rule 5: the data-model split shipped across M4 D3 (registry Instance rows) + D9-5 (home/adapter on the endpoint record); this umbrella req activates when instantiate-anywhere makes the split user-facing (instance minting) — which DEFERS PAST M5 (user decision 2026-06-04, M5-PLAN scope decision 2: consent framework seam only); its concrete slices are tracked by REQ-INST-7..15
187	[[requirements]]
188	id = "REQ-INST-2"
189	title = "Per-node files, synced Psyche mind"
190	required_stages = ["impl", "unit"]  # activated M4-D6a: the mind now lives in the tracked/ two-tier store (spt-store::contextstore over BranchStore), physically separate from never-synced node-local perches/ — spt_live::ingest::route_slices routes commune drops' <live-context>/<project-context> slices into a-<id>/p-<project_id> branch files (untagged body → live, the parser fallback; precedence-guarded 6.5; checkpoint commit per written tier); signoff::write_resume_commune stamps Self provenance per slice through the same router; resume::download_psyche_context composes the two tiers back into the two-slice envelope (pure read, never mints store state; other-project download sees live only). Daemon lifecycle derives project_id from the commune dir's parent (cwd fallback). The cross-node SYNC of these branches = D6c (REQ-NET-3/REQ-INST-5); int = D9 two-host. unit = per-tier routing/no-leak + untagged→live + suppressed-consumed + resume-commune both-tiers + download compose/cross-project-isolation + e2e listen→ingest→live-tier
191	[[requirements]]

required_stages = ["impl", "unit", "int"]  # activated M5-D3a; int activated M5-D9b: the D3e shell E2E + the real-shell E2E (notify_shell_e2e.rs, CI-hook-driven against the standalone spt-shell-notify adapter) carry the int tags
[[requirements]]
id = "REQ-EP-6"
title = "Gateway type acceptance: a Gateway-typed perch binds (api bind --type, open type system — un-hardcode the live_agent default), advertises/addressable like any endpoint, owns shells (owner validation not agent-family-gated), subscribes to digests, and is the user-msg identity gate's user-backed origin (REQ-MSG-5); in-tree mock-gateway fixture (R-DOCS-2 pattern, no downstream adapter code). Cross-node WAN Gateway-origin (registry endpoint_type trust) = documented fail-closed residual, deferred past M9 (doyle G2 ruling 2026-06-13)"
required_stages = ["doc", "impl", "unit"]  # activated 2026-06-13: M9-T5 (CONTEXT.md §Gateway; api bind --type un-hardcode at establish_perch; gateway-owns-shell + local user-backed-origin E2E via mock-gateway fixture; the WAN fail-closed row tested at the receive_wan funnel)
[[requirements]]
id = "REQ-EP-7"
title = "Durable live-role.md: a per-agent broad-purpose statement in tracked/agents/<id>/ beside live-context.md (replicates with the mind on the same a-<id> branch); renders FIRST at start-transition context injection (role -> live-context -> project-context); SOLE writer `spt endpoint role --overwrite <file>` — mechanical no-automated-writer guarantee (echo-commune ingest / signoff / Psyche reconcile structurally exclude it). The user-backed-origin hard gate on the writer is a deferred later tightening (rides the user-msg identity plumbing)"
required_stages = ["doc", "impl", "unit"]  # activated 2026-06-13: M9-T6 (CONTEXT.md §live role; contextstore live_role_file/live_role_path seam; resume::download_psyche_context role-first injection order; spt endpoint role print/--overwrite sole writer; injection-order + no-automated-writer tests; M9-WAVE2-ROLE-WRITERS.md survey)

# ───────────────────────────── Instances ────────────────────────────────────
[[requirements]]
id = "REQ-INST-1"
title = "endpoint ID vs instance split (adapter-agnostic ID)"
required_stages = []  # rule 5: the data-model split shipped across M4 D3 (registry Instance rows) + D9-5 (home/adapter on the endpoint record); this umbrella req activates when instantiate-anywhere makes the split user-facing (instance minting) — which DEFERS PAST M5 (user decision 2026-06-04, M5-PLAN scope decision 2: consent framework seam only); its concrete slices are tracked by REQ-INST-7..15
[[requirements]]
id = "REQ-INST-2"
title = "Per-node files, synced Psyche mind"
required_stages = ["impl", "unit"]  # activated M4-D6a: the mind now lives in the tracked/ two-tier store (spt-store::contextstore over BranchStore), physically separate from never-synced node-local perches/ — spt_live::ingest::route_slices routes commune drops' <live-context>/<project-context> slices into a-<id>/p-<project_id> branch files (untagged body → live, the parser fallback; precedence-guarded 6.5; checkpoint commit per written tier); signoff::write_resume_commune stamps Self provenance per slice through the same router; resume::download_psyche_context composes the two tiers back into the two-slice envelope (pure read, never mints store state; other-project download sees live only). Daemon lifecycle derives project_id from the commune dir's parent (cwd fallback). The cross-node SYNC of these branches = D6c (REQ-NET-3/REQ-INST-5); int = D9 two-host. unit = per-tier routing/no-leak + untagged→live + suppressed-consumed + resume-commune both-tiers + download compose/cross-project-isolation + e2e listen→ingest→live-tier
[[requirements]]


270	id = "REQ-MSG-4"
271	title = "Listener stream stdout emits EVENT envelope lines (sister-format, ADR-0001): parse the __REPLY_TO__ frame, pass pre-formed typed envelopes through verbatim (no double-wrap), compose <EVENT type=\"msg\" from=…> otherwise, chunk oversized lines into EVENT-PART"
272	required_stages = ["impl", "unit", "int"]  # activated 2026-06-06 (envelope-missing fix): spt-msg::emit render seam + both listener loops (spt ready run_listen, spt api listen cmd_listen); int = quickstart E2E asserts the envelope shape on the live + backlog paths. Hook-channel drains (api poll / worker-poll) stay raw-frame by contract.
273	[[requirements]]
274	id = "REQ-MSG-5"
275	title = "user-msg envelope kind + daemon identity gate: a Gateway endpoint / the local user's CLI author user-msg (the user's authority); agent-family senders re-stamped to plain msg; identity-gated never payload-trusted (KH 7.3/7.5); wire-additive (N-1 receivers tolerate the new type)"
276	required_stages = ["doc", "impl", "unit"]  # activated 2026-06-13: M9-T4 (CONTEXT.md §Gateway user-msg; spt-proto EVENT_TYPE_USER_MSG + compose_user_msg_event + the MsgOrigin identity gate / gate_user_msg_type truth table incl. spoof rows; wire-additive N-1 tolerance fixture). Daemon origination/WAN-ingress wiring + render rides the same activation.
277
278	# ───────────────────────────── Networking / pairing ─────────────────────────
279	[[requirements]]
280	id = "REQ-NODE-IDENTITY"
281	title = "Ed25519 identity primitive: keypair, detached sign/verify, stable pubkey<->hex"
282	required_stages = ["impl", "unit"]  # activated M0: spt-proto identity primitive (T5); foundation for R-NET/R-PAIR/SptNode
283	[[requirements]]
284	id = "REQ-NET-1"
285	title = "WAN messaging first-class, behind default-on net feature flag"
286	required_stages = ["impl", "unit", "int"]  # activated M4-D1: spt-net NetEndpoint binds an iroh endpoint to the node's own spt-proto Ed25519 identity (EndpointId == node pubkey) behind the default-on `net` feature, with connect/accept over the SPT_NET_ALPN; unit proves identity-binding + a hermetic loopback QUIC bidi echo. int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — a real WAN message lands in the remote perch spool, HFENDULEAM↔gravity over tailscale, [twohost] CI run)
287	[[requirements]]
288	id = "REQ-NET-2"
289	title = "n0 relay default + self-host knob + plain-language disclosure"

id = "REQ-MSG-4"
title = "Listener stream stdout emits EVENT envelope lines (sister-format, ADR-0001): parse the __REPLY_TO__ frame, pass pre-formed typed envelopes through verbatim (no double-wrap), compose <EVENT type=\"msg\" from=…> otherwise, chunk oversized lines into EVENT-PART"
required_stages = ["impl", "unit", "int"]  # activated 2026-06-06 (envelope-missing fix): spt-msg::emit render seam + both listener loops (spt ready run_listen, spt api listen cmd_listen); int = quickstart E2E asserts the envelope shape on the live + backlog paths. Hook-channel drains (api poll / worker-poll) stay raw-frame by contract.
[[requirements]]
id = "REQ-MSG-5"
title = "user-msg envelope kind + daemon identity gate: a Gateway endpoint / the local user's CLI author user-msg (the user's authority); agent-family senders re-stamped to plain msg; identity-gated never payload-trusted (KH 7.3/7.5); wire-additive (N-1 receivers tolerate the new type)"
required_stages = ["doc", "impl", "unit"]  # activated 2026-06-13: M9-T4 (CONTEXT.md §Gateway user-msg; spt-proto EVENT_TYPE_USER_MSG + compose_user_msg_event + the MsgOrigin identity gate / gate_user_msg_type truth table incl. spoof rows; wire-additive N-1 tolerance fixture). Daemon origination/WAN-ingress wiring + render rides the same activation.

# ───────────────────────────── Networking / pairing ─────────────────────────
[[requirements]]
id = "REQ-NODE-IDENTITY"
title = "Ed25519 identity primitive: keypair, detached sign/verify, stable pubkey<->hex"
required_stages = ["impl", "unit"]  # activated M0: spt-proto identity primitive (T5); foundation for R-NET/R-PAIR/SptNode
[[requirements]]
id = "REQ-NET-1"
title = "WAN messaging first-class, behind default-on net feature flag"
required_stages = ["impl", "unit", "int"]  # activated M4-D1: spt-net NetEndpoint binds an iroh endpoint to the node's own spt-proto Ed25519 identity (EndpointId == node pubkey) behind the default-on `net` feature, with connect/accept over the SPT_NET_ALPN; unit proves identity-binding + a hermetic loopback QUIC bidi echo. int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — a real WAN message lands in the remote perch spool, HFENDULEAM↔gravity over tailscale, [twohost] CI run)
[[requirements]]
id = "REQ-NET-2"
title = "n0 relay default + self-host knob + plain-language disclosure"


REQ survey done. Plan: REQ-MSG-6 folds the Instance.endpoint_type model (cross-refs REQ-INST-7); activate stages per-commit to keep traceable green. Mint REQ-MSG-6 + retitle REQ-EP-6.


Edit

required_stages = ["doc", "impl", "unit"]  # activated 2026-06-13: M9-T4 (CONTEXT.md §Gateway user-msg; spt-proto EVENT_TYPE_USER_MSG + compose_user_msg_event + the MsgOrigin identity gate / gate_user_msg_type truth table incl. spoof rows; wire-additive N-1 tolerance fixture). Daemon origination/WAN-ingress wiring + render rides the same activation.

# ───────────────────────────── Networking / pairing ─────────────────────────

required_stages = ["doc", "impl", "unit"]  # activated 2026-06-13: M9-T4 (CONTEXT.md §Gateway user-msg; spt-proto EVENT_TYPE_USER_MSG + compose_user_msg_event + the MsgOrigin identity gate / gate_user_msg_type truth table incl. spoof rows; wire-additive N-1 tolerance fixture). Daemon origination/WAN-ingress wiring + render rides the same activation.
[[requirements]]
id = "REQ-MSG-6"
title = "cross-node Gateway user-msg honored via advertised endpoint_type: a user-msg from a Gateway-typed origin survives the receive_wan funnel as user-msg (vs the fail-closed re-stamp), keyed on the QUIC-handshake-proven origin node (never wire `from`). Trust boundary = subnet membership (operator-ratified 2026-06-13); no defense against an in-subnet member forging the type. Instance.endpoint_type is an additive serde-default field extending REQ-INST-7's data model. Absent/unknown type → re-stamp (N-1 rollout grace)"
required_stages = ["doc"]  # activated 2026-06-13: M9 cross-node Gateway WAN (CONTEXT.md §Gateway posture rewrite, doc commit 30fea7f). impl/unit activate as the branch lands the Instance.endpoint_type field + advertise populate + receive_wan resolve + the origin matrix.

# ───────────────────────────── Networking / pairing ─────────────────────────


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

required_stages = ["doc", "impl", "unit"]  # activated 2026-06-13: M9-T4 (CONTEXT.md §Gateway user-msg; spt-proto EVENT_TYPE_USER_MSG + compose_user_msg_event + the MsgOrigin identity gate / gate_user_msg_type truth table incl. spoof rows; wire-additive N-1 tolerance fixture). Daemon origination/WAN-ingress wiring + render rides the same activation.

# ───────────────────────────── Networking / pairing ─────────────────────────

required_stages = ["doc", "impl", "unit"]  # activated 2026-06-13: M9-T4 (CONTEXT.md §Gateway user-msg; spt-proto EVENT_TYPE_USER_MSG + compose_user_msg_event + the MsgOrigin identity gate / gate_user_msg_type truth table incl. spoof rows; wire-additive N-1 tolerance fixture). Daemon origination/WAN-ingress wiring + render rides the same activation.
[[requirements]]
id = "REQ-MSG-6"
title = "cross-node Gateway user-msg honored via advertised endpoint_type: a user-msg from a Gateway-typed origin survives the receive_wan funnel as user-msg (vs the fail-closed re-stamp), keyed on the QUIC-handshake-proven origin node (never wire `from`). Trust boundary = subnet membership (operator-ratified 2026-06-13); no defense against an in-subnet member forging the type. Instance.endpoint_type is an additive serde-default field extending REQ-INST-7's data model. Absent/unknown type → re-stamp (N-1 rollout grace)"
required_stages = ["doc"]  # activated 2026-06-13: M9 cross-node Gateway WAN (CONTEXT.md §Gateway posture rewrite, doc commit 30fea7f). impl/unit activate as the branch lands the Instance.endpoint_type field + advertise populate + receive_wan resolve + the origin matrix.

# ───────────────────────────── Networking / pairing ─────────────────────────

title = "user-msg envelope kind + daemon identity gate: a Gateway endpoint / the local user's CLI author user-msg (the user's authority); agent-family senders re-stamped to plain msg; identity-gated never payload-trusted (KH 7.3/7.5); wire-additive (N-1 receivers tolerate the new type)"

+title = "cross-node Gateway user-msg honored via advertised endpoint_type: a user-msg from a Gateway-typed origin survives the receive_wan funnel as user-msg (vs the fail-closed re-stamp), keyed on the QUIC-handshake-proven origin node (never wire `from`). Trust boundary = subnet membership (operator-ratified 2026-06-13); no defense against an in-subnet member forging the type. Instance.endpoint_type is an additive serde-default field extending REQ-INST-7's data model. Absent/unknown type → re-stamp (N-1 rollout grace)"

+required_stages = ["doc"]  # activated 2026-06-13: M9 cross-node Gateway WAN (CONTEXT.md §Gateway posture rewrite, doc commit 30fea7f). impl/unit activate as the branch lands the Instance.endpoint_type field + advertise populate + receive_wan resolve + the origin matrix.


subscribes to digests, and is the user-msg identity gate's user-backed origin (REQ-MSG-5); in-tree mock-gateway fixture (R-DOCS-2 pattern, no downstream adapter code). Cross-node WAN Gateway-origin (registry endpoint_type trust) = documented fail-closed residual, deferred past M9 (doyle G2 ruling 2026-06-13)"

subscribes to digests, and is the user-msg identity gate's user-backed origin (REQ-MSG-5); in-tree mock-gateway fixture (R-DOCS-2 pattern, no downstream adapter code). Cross-node WAN Gateway-origin (registry endpoint_type trust) tracked by REQ-MSG-6"


required_stages = ["impl", "unit", "int"]  # activated M5-D3a; int activated M5-D9b: the D3e shell E2E + the real-shell E2E (notify_shell_e2e.rs, CI-hook-driven against the standalone spt-shell-notify adapter) carry the int tags

-title = "Gateway type acceptance: a Gateway-typed perch binds (api bind --type, open type system — un-hardcode the live_agent default), advertises/addressable like any endpoint, owns shells (owner validation not agent-family-gated), subscribes to digests, and is the user-msg identity gate's user-backed origin (REQ-MSG-5); in-tree mock-gateway fixture (R-DOCS-2 pattern, no downstream adapter code). Cross-node WAN Gateway-origin (registry endpoint_type trust) = documented fail-closed residual, deferred past M9 (doyle G2 ruling 2026-06-13)"

+title = "Gateway type acceptance: a Gateway-typed perch binds (api bind --type, open type system — un-hardcode the live_agent default), advertises/addressable like any endpoint, owns shells (owner validation not agent-family-gated), subscribes to digests, and is the user-msg identity gate's user-backed origin (REQ-MSG-5); in-tree mock-gateway fixture (R-DOCS-2 pattern, no downstream adapter code). Cross-node WAN Gateway-origin (registry endpoint_type trust) tracked by REQ-MSG-6"

required_stages = ["doc", "impl", "unit"]  # activated 2026-06-13: M9-T5 (CONTEXT.md §Gateway; api bind --type un-hardcode at establish_perch; gateway-owns-shell + local user-backed-origin E2E via mock-gateway fixture; the WAN fail-closed row tested at the receive_wan funnel)


Now add `[doc->REQ-MSG-6]` in CONTEXT §Gateway posture. Find it.


Bash

grep -n "doc->REQ-MSG-5\|doc->REQ-MSG-6\|subnet member\|trust boundary\|proven origin node\|endpoint_type" CONTEXT.md

Find gateway posture doc tag in CONTEXT