Files
alknet/tasks/core/bidistreamsource-trait.md
T
glm-5.2 1318eee451 docs(task): mark bidistreamsource-trait acceptance criteria complete + add summary
The implementer flipped the frontmatter status to completed but left the
acceptance checkboxes empty and omitted the Summary section. Fill both
to match the convention every other completed task in tasks/core/ follows
(core-types.md, fingerprint-normalization.md, review-core.md — all check
boxes [x] and add a ## Summary section recording what landed).
2026-07-12 10:06:02 +00:00

256 lines
11 KiB
Markdown

---
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<dyn BidiStreamSource>`. 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<Option<(SendStream, RecvStream)>>,
remote_addr: Option<SocketAddr>,
}
pub struct Connection {
kind: ConnectionKind,
alpn: Vec<u8>,
identity: OnceLock<Identity>,
}
```
`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<SocketAddr>;
fn close(&self, code: u32, reason: &str);
}
pub struct Connection {
source: Box<dyn BidiStreamSource>,
alpn: Vec<u8>,
identity: OnceLock<Identity>,
}
```
`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<Option<...>>` 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<dyn BidiStreamSource>` 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<Vec<u8>>) -> 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<dyn BidiStreamSource>` + `alpn: Vec<u8>` + `identity: OnceLock<Identity>`
- [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<Vec<u8>>)` 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<dyn BidiStreamSource>` 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<dyn BidiStreamSource>` 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`.