--- id: core/bidistreamsource-trait name: "Implement BidiStreamSource trait + AuthContext::anonymous (ADR-070, REQ-CORE-01/02/03)" status: completed depends_on: [] scope: broad risk: medium impact: component level: implementation --- ## Description Land the three core-crate changes surfaced by the alknet-channels POC (`docs/research/alknet-channels/poc-summary.md` §"Issues Surfaced" #1-3). The architecture specs and ADR-070 are already written; this task is the implementation. ### Part 1: `BidiStreamSource` trait (REQ-CORE-01, ADR-070) Extract the stream-yield operations from `Connection` into a trait so downstream crates (channels, future transports) can add connection shapes without editing `alknet-core`. `Connection` goes from a closed enum (`ConnectionKind: Quinn | Iroh | Stream`) to holding `Box`. The public `Connection` API is preserved verbatim — this is an internal refactor, not a handler-facing change. #### Current state (`crates/alknet-core/src/types.rs`) ```rust enum ConnectionKind { #[cfg(feature = "quinn")] Quinn(quinn::Connection), #[cfg(feature = "iroh")] Iroh(iroh::endpoint::Connection), Stream(StreamConn), } struct StreamConn { stream: Mutex>, remote_addr: Option, } pub struct Connection { kind: ConnectionKind, alpn: Vec, identity: OnceLock, } ``` `accept_bi`, `open_bi`, `remote_addr`, `close` all match on `self.kind`. #### Target state (ADR-070) ```rust #[async_trait] pub trait BidiStreamSource: Send + Sync + 'static { async fn accept_bi(&self) -> Result<(SendStream, RecvStream), StreamError>; async fn open_bi(&self) -> Result<(SendStream, RecvStream), StreamError>; fn remote_addr(&self) -> Option; fn close(&self, code: u32, reason: &str); } pub struct Connection { source: Box, alpn: Vec, identity: OnceLock, } ``` `Connection::accept_bi` / `open_bi` / `remote_addr` / `close` delegate to `self.source`. `remote_alpn` reads `self.alpn` (unchanged). `set_identity` / `identity` read/write `self.identity` (unchanged). #### Crate-private implementations Three impls, each wrapping an existing constructor: | Impl | Constructor(s) | Yield semantics | |------|----------------|-----------------| | `QuinnBidiStreamSource` | `from_quinn` / `from_quinn_with_alpn` (feature `quinn`) | many streams | | `IrohBidiStreamSource` | `from_iroh` (feature `iroh`) | many streams | | `StreamBidiStreamSource` | `from_stream` / `from_bidi` (no feature gate) | yield-once, then `ConnectionClosed`; `open_bi` returns `StreamClosed` | The `StreamBidiStreamSource` preserves the ADR-065 yield-once contract exactly — `accept_bi` takes the stream from the `Mutex>` on the first call, returns `ConnectionClosed` on subsequent calls. `open_bi` returns `StreamError::StreamClosed`. `close` drops the stream (ignores `code`/`reason` — see Part 2). `remote_addr` returns the stored `SocketAddr`. The `QuinnBidiStreamSource` / `IrohBidiStreamSource` impls delegate to the underlying `quinn::Connection` / `iroh::endpoint::Connection` exactly as the current `ConnectionKind::Quinn` / `Iroh` match arms do — same error mapping (`map_quinn_connection_error` / `map_iroh_connection_error`), same `SendStream`/`RecvStream` construction (`from_quinn` / `from_iroh`). #### What does NOT change - `ProtocolHandler` trait — unchanged. - `HandlerRegistry` — unchanged. - All handler code (`HttpAdapter`, `TtyAdapter`, `CallAdapter`) — unchanged. They receive a `Connection` and call `accept_bi` / `open_bi` on it. The dispatch through `Box` is transparent. - `SendStream` / `RecvStream` — unchanged (their own internal enum dispatch stays). - `BiStream` trait — unchanged. - The endpoint's accept loops — unchanged (they call `from_quinn` / `from_iroh`, which now internally wrap a `BidiStreamSource` impl). - `Connection::remote_alpn` / `set_identity` / `identity` / `from_bidi` — unchanged. ### Part 2: `close()` params fix (REQ-CORE-02, folded into ADR-070) The `close(&self, code: u32, reason: &str)` signature stays on the trait (non-QUIC impls ignore the args). This resolves the ADR-065 leftover clippy warning: under `--no-default-features`, the `Stream` backend's `close(code, reason)` takes both args and uses neither, which clippy flags as two unused variable warnings on `types.rs:500`. Under the trait, the `StreamBidiStreamSource::close` impl prefixes the args with `_code` / `_reason` and carries a doc comment stating they're ignored because the drop is the close (ADR-065). The warning disappears; the signature matches the public `Connection::close` API (no caller breakage). See ADR-070 §"REQ-CORE-02" for the full rationale of why the QUIC-shaped signature stays on the trait rather than being split. ### Part 3: `AuthContext::anonymous` constructor (REQ-CORE-03) Add a convenience constructor to `AuthContext` in `crates/alknet-core/src/auth.rs`: ```rust impl AuthContext { /// Construct an `AuthContext` with no identity, no fingerprint, and no /// remote address — only the ALPN is set. For POCs, tests, and handlers /// that don't require auth. pub fn anonymous(alpn: impl Into>) -> Self { Self { identity: None, alpn: alpn.into(), remote_addr: None, tls_client_fingerprint: None, } } } ``` Not gated behind a `test-utils` feature — it's a plain `pub fn` useful for any caller that constructs an `AuthContext` outside the endpoint's resolution path. The name is honest about the semantics: no identity, no fingerprint. This removes the four-`None`-field literal that recurred in every handler POC and test (`poc-summary.md` §"Issues Surfaced" #3). ### Commit structure This is one task but may be two commits if the implementer prefers to separate concerns: 1. `BidiStreamSource` trait + `Connection` refactor + `close()` fix (Parts 1+2, `types.rs`) 2. `AuthContext::anonymous` (Part 3, `auth.rs`) Or one commit covering all three. Either is acceptable — the changes are in different files but logically related (all from the same POC findings). ## Acceptance Criteria ### Part 1: BidiStreamSource - [x] `BidiStreamSource` trait defined with `accept_bi`, `open_bi`, `remote_addr`, `close` (all async or sync per ADR-070) - [x] `BidiStreamSource: Send + Sync + 'static` (object-safe) - [x] `Connection` struct holds `Box` + `alpn: Vec` + `identity: OnceLock` - [x] `QuinnBidiStreamSource` implements `BidiStreamSource` (feature-gated `quinn`) - [x] `IrohBidiStreamSource` implements `BidiStreamSource` (feature-gated `iroh`) - [x] `StreamBidiStreamSource` implements `BidiStreamSource` (no feature gate) - [x] `StreamBidiStreamSource::accept_bi` yields once, then `ConnectionClosed` (ADR-065 contract preserved) - [x] `StreamBidiStreamSource::open_bi` returns `StreamClosed` - [x] `Connection::from_quinn` / `from_quinn_with_alpn` / `from_iroh` / `from_stream` / `from_bidi` all preserved (same signatures, same behavior) - [x] `Connection::accept_bi` / `open_bi` / `remote_addr` / `close` delegate to `self.source` - [x] `Connection::remote_alpn` / `set_identity` / `identity` unchanged (read `self.alpn` / `self.identity`) - [x] `ConnectionKind` enum removed (replaced by the trait object) - [x] `map_quinn_connection_error` / `map_iroh_connection_error` preserved (used by the QUIC impls) - [x] Existing `types.rs` tests pass unchanged (the `test_connection()` helper, `set_identity` tests, `remote_alpn`/`remote_addr` tests, `StreamError` mapping tests) ### Part 2: close() fix - [x] `StreamBidiStreamSource::close` prefixes `code`/`reason` with `_` and documents why they're ignored - [x] `cargo clippy -p alknet-core --no-default-features` passes with **no warnings** (the ADR-065 leftover is fixed) - [x] `cargo clippy -p alknet-core` (default features) passes with no warnings - [x] `cargo clippy -p alknet-core --all-features` passes with no warnings (if applicable) ### Part 3: AuthContext::anonymous - [x] `AuthContext::anonymous(alpn: impl Into>)` constructor added to `auth.rs` - [x] Sets `identity: None`, `alpn: alpn.into()`, `remote_addr: None`, `tls_client_fingerprint: None` - [x] Unit test: `AuthContext::anonymous(b"alknet/test")` produces the expected fields - [x] Existing `auth.rs` tests pass unchanged ### All parts - [x] `cargo test -p alknet-core` succeeds (all feature combos) - [x] `cargo test -p alknet-core --no-default-features` succeeds - [x] `cargo clippy -p alknet-core` succeeds with no warnings - [x] `cargo clippy -p alknet-core --no-default-features` succeeds with no warnings - [x] `cargo fmt --check -p alknet-core` passes - [x] Downstream crates compile unchanged: `cargo check -p alknet-call` succeeds (no handler code change needed) ## References - docs/architecture/decisions/070-bidistreamsource-trait.md — ADR-070 (the trait + close() rationale) - docs/architecture/decisions/065-connection-from-stream-generic-single-stream.md — ADR-065 (the yield-once contract the StreamBidiStreamSource preserves) - docs/architecture/crates/core/core-types.md — Connection + BidiStreamSource spec (updated) - docs/architecture/crates/core/auth.md — AuthContext::anonymous spec (updated) - docs/research/alknet-channels/poc-summary.md — §"Issues Surfaced" #1-3 (the POC findings) - docs/research/alknet-channels/phase-0-findings.md — §POC-Validated Requirements (REQ-CORE-01/02/03) ## Notes > The `BidiStreamSource` refactor is the load-bearing change. It touches > `Connection` — the struct every handler depends on — but the public API > is preserved verbatim, so no handler code changes. The risk is in the > internal dispatch: `Box` adds one method-call > indirection per `accept_bi`/`open_bi`/`close`/`remote_addr`, which is > negligible next to the async I/O those operations perform. The > `ConnectionKind` enum is removed entirely; the three variants become > three trait impls. The clippy warning under `--no-default-features` > (unused `code`/`reason` on the Stream backend's `close`) is fixed > structurally by the trait — the `StreamBidiStreamSource::close` impl > documents why the args are ignored, and the `_` prefix is intentional. > Verify all four feature combinations pass clippy (default, quinn-only, > iroh-only, no-default-features) — the warning only surfaces under > `--no-default-features` today. ## Summary Implemented all three parts in `crates/alknet-core/src/types.rs` and `auth.rs`. Extracted `BidiStreamSource` trait (`accept_bi`, `open_bi`, `remote_addr`, `close`); replaced `ConnectionKind` enum + `StreamConn` with `Box` on `Connection`. Three crate-private impls: `QuinnBidiStreamSource` (feature `quinn`), `IrohBidiStreamSource` (feature `iroh`), `StreamBidiStreamSource` (no gate, yield-once per ADR-065). All `Connection` constructors and the public API preserved verbatim — handlers dispatch through the trait object transparently. `StreamBidiStreamSource::close(_code, _reason)` prefixes the args and documents why they're ignored, resolving the ADR-065 leftover clippy warning under `--no-default-features` (REQ-CORE-02). Added `AuthContext::anonymous(alpn)` to `auth.rs` with a unit test (REQ-CORE-03). Verified: 146 tests pass (default features), 114 (`--no-default-features`), 128 (`--features iroh`); clippy clean across default, `--no-default-features`, `--features iroh`, `--all-features` (zero warnings); `cargo check -p alknet-call` compiles unchanged; `cargo fmt --check` passes. Merged to develop as `60cce22`.