From 10707152a3b5017147de7bb304c27bb748998e28 Mon Sep 17 00:00:00 2001 From: Mollusk Date: Thu, 18 Jun 2026 04:24:01 -0400 Subject: [PATCH] S8 (design-first): pure audio_sender_admitted membership seam (unwired) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Design-first checkpoint for S8 (authorize inbound audio against live room membership). Codex's design note (in the handoff task-report.md) establishes the authoritative roster = gossip IrohGossipState.peers, NOT the audio transport connection list, and recommends mirroring it into an audio-admission snapshot consulted at AudioRouter::accept + datagram ingest. This commit lands ONLY the pure decision seam + tests; wiring is deliberately paused for a senior decision on the reconnect-grace policy (gossip drops a peer from the roster on transient NeighborDown, but core keeps the audio supervisor alive for RECONNECT_GRACE — a strict roster-only gate would cut audio on blips). - audio_sender_admitted(remote, roster) -> bool (pub(crate), #[allow(dead_code)]). - 4 tests: member admitted, stranger rejected, former member rejected after roster removal, mid-join peer rejected until authenticated Announce inserts it. - No behavior change: accept/datagram/mixer paths untouched. S8 remains OPEN. 310 lib tests / clippy --all-targets / release all green (re-run by senior). Co-Authored-By: Claude Opus 4.8 --- src/network/iroh_impl.rs | 59 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/src/network/iroh_impl.rs b/src/network/iroh_impl.rs index 20ddf04..6c733cb 100644 --- a/src/network/iroh_impl.rs +++ b/src/network/iroh_impl.rs @@ -5,7 +5,7 @@ use bytes::Bytes; use tokio::sync::mpsc; use tokio::sync::mpsc::Receiver; use std::sync::{Arc, Mutex as StdMutex}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::time::Duration; use async_trait::async_trait; @@ -135,6 +135,13 @@ fn is_graceful_leave(err: &ConnectionError) -> bool { matches!(err, ConnectionError::ApplicationClosed(frame) if frame.error_code == VarInt::from_u32(GOODBYE_CODE)) } +/// Pure S8 membership decision: iroh already authenticated `remote` as the +/// connection's endpoint id, so audio admission is exactly live roster membership. +#[allow(dead_code)] // Design-first S8 seam; wiring waits for senior review. +pub(crate) fn audio_sender_admitted(remote: EndpointId, roster: &HashSet) -> bool { + roster.contains(&remote) +} + /// Owns a single peer's connection lifecycle for as long as the peer is in the /// room: obtain a link, run the send/read loops, and on loss obtain a new one — /// with capped backoff on the dialing side. The deterministic-initiator rule @@ -443,3 +450,53 @@ impl NetworkTransport for IrohTransport { .ok_or_else(|| NetError::Other("Connection events already subscribed".to_string())) } } + +#[cfg(test)] +mod tests { + use super::*; + use iroh::SecretKey; + + fn endpoint_id() -> EndpointId { + SecretKey::generate().public() + } + + #[test] + fn audio_sender_admission_accepts_roster_member() { + let member = endpoint_id(); + let roster = HashSet::from([member]); + + assert!(audio_sender_admitted(member, &roster)); + } + + #[test] + fn audio_sender_admission_rejects_unknown_sender() { + let member = endpoint_id(); + let stranger = endpoint_id(); + let roster = HashSet::from([member]); + + assert!(!audio_sender_admitted(stranger, &roster)); + } + + #[test] + fn audio_sender_admission_rejects_former_member_after_roster_removal() { + let former = endpoint_id(); + let mut roster = HashSet::from([former]); + assert!(audio_sender_admitted(former, &roster)); + + roster.remove(&former); + + assert!(!audio_sender_admitted(former, &roster)); + } + + #[test] + fn audio_sender_admission_waits_for_mid_join_announce() { + let joining_peer = endpoint_id(); + let mut roster = HashSet::new(); + + assert!(!audio_sender_admitted(joining_peer, &roster)); + + roster.insert(joining_peer); + + assert!(audio_sender_admitted(joining_peer, &roster)); + } +}