Files
alknet/docs/research/alknet-channels/poc-summary.md
T
glm-5.2 f997c81d2f docs(research): note Connection::from_source gap closed (e8bbc74); POC deliberately not retrofitted
The from_source constructor gap surfaced by this POC has been resolved by
commit e8bbc74 (pub fn from_source(impl BidiStreamSource, alpn) at types.rs:574).
Updated the POC scope note on issue #1 to record this and explain why the POC
was deliberately not retrofitted to use from_source + a ChannelBidiStreamSource:
the POC's de-risk objective was reached, the surfaced issues are resolved, and
rewiring to the N-stream shape now would be rework the Phase 1 crate will do
authoritatively anyway. POC stands as the de-risk artifact; Phase 1 builds on
the now-unblocked trait and constructor.
2026-07-12 11:11:53 +00:00

516 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
status: complete
last_updated: 2026-07-12
---
# alknet-channels: POC Research Summary
**Status:** Research complete — all three high-leverage unknowns validated. The
approach is viable; the remaining unknowns are spec-scope, not feasibility.
**Re-verified 2026-07-12** against the post-refactor `alknet-core`
(`BidiStreamSource` trait, ADR-070): all 28 tests still pass, clippy fully
clean, and the POC now uses `AuthContext::anonymous`. The three `alknet-core`
issues the POC surfaced (#1, #2, #3 below) are resolved by ADR-070 /
commit `60cce22`; the four `alknet-channels`-side issues (#4–#7) remain for
Phase 1.
**Date:** 2026-07-12
**Scope:** Captures what the POC proved about the 9-byte chunk format, N-channel
demux/mux, per-channel `Connection` presentation, and the tunnel handler — and
what it surfaced for the coming `alknet-channels` crate spec and the light
`alknet-core` refactor that should land alongside it.
---
## Executive Summary
A POC (`alknet-channels-poc`, `/workspace/alknet-channels-poc`) validated the
three highest-leverage unknowns of the channels layer over a
`tokio::io::duplex` stand-in transport:
1. **Chunk format + N-channel demux/mux** — the 9-byte format
(`[channel_id:u32 be][stream_type:u8][length:u32 be][payload]`) is a clean
generalization of TTY's 5-byte format. The sync core (`parse_header`/
`write_header`) is pure, WASM-compatible by construction. The mpsc-bridged
async shell scales to N concurrent channels with per-channel order
preservation and cross-channel isolation, and bounded-buffer backpressure
(DP-5 option c) works for the simple case.
2. **Per-channel `Connection` presentation** — `Connection::from_stream` (from
the transport-generalization work) is sufficient to present each channel as
a `Connection` a `ProtocolHandler` can drive unchanged. A minimal echo
handler runs through the full demux→Connection→handler→mux path with zero
channels-layer awareness. The yield-once `accept_bi` contract composes for
handlers that loop `accept_bi` (like `TtyAdapter`).
3. **Tunnel handler** — the bidirectional pump pattern (same shape as TTY's
`pump_session`, two pumps instead of three) works as a generic port proxy
through channels. A `TunnelHandler` of ~15 lines proxies a channel to a
local TCP echo server with no chunk-format or channels-layer awareness, and
a tunnel channel coexists with an echo channel on one channels connection
without interference.
**28 tests pass** (5 wire codec + 7 mpsc stream adapters + 5 demux + 3 mux + 5
echo handler + 3 tunnel handler). Clippy is fully clean for the POC's own code
and for the updated `alknet-core` dependency (the ADR-065 `Connection::close`
unused-arg warnings the POC originally surfaced are resolved by ADR-070 — see
§"Issues Surfaced" #2).
**Stretch goal validated:** the sync core (`wire.rs`) compiles cleanly under
`wasm32-unknown-unknown`. The "WASM compatibility by construction" claim holds
for the pure-byte-manipulation core; the async shell and the `alknet-core`
dependency graph are NOT WASM-clean yet (transitive `getrandom`/`rand` deps),
which is a Phase 1 concern, not a POC concern.
The POC depends on `alknet-core` only (path dependency,
`default-features = false` — no QUIC needed for the POC), and uses
`tokio::io::duplex` as the stand-in transport. It does NOT depend on
`alknet-tty`, `alknet-call`, or any other handler crate. The echo and tunnel
handlers are POC-local stubs, not the real adapters.
---
## The Sync Core / Async Shell Split (Confirmed)
The POC confirms the split proposed in `poc-plan.md` §"Design Principles":
- **Sync core (`wire.rs`)**: `parse_header(&[u8; 9]) -> ChunkHeader` and
`write_header(channel_id, stream_type, length, &mut [u8; 9])`. Pure
functions, no async, no platform deps. Validates `MAX_CHUNK_LEN` (matches
TTY's 16 MiB cap, preserving the framing-disambiguation soundness property
from ADR-052 §5). Compiles under `wasm32-unknown-unknown`.
- **Async shell (`demux.rs` / `mux.rs`)**: `Demux::run` reads 9-byte headers +
payloads off the transport via `read_exact`, routes payloads to per-
`(channel_id, stream_type)` bounded `mpsc::Sender<Bytes>`. `Mux` frames
`Bytes` from per-channel `mpsc::Receiver<Bytes>` back onto the transport.
- **Per-channel reassembly (`mpsc_stream.rs`)**: `MpscRecvStream`
(`mpsc::Receiver<Bytes>` → `AsyncRead`, ~50 lines) and `MpscSendStream`
(`mpsc::Sender<Bytes>` → `AsyncWrite` with backpressure, ~60 lines). These
back the `RecvStream`/`SendStream` the handler sees via
`RecvStream::from_stream` / `SendStream::from_stream`.
The split is as clean as the TTY POC's was. The "don't have to async" pattern
lives in the demux/mux shell; the core byte manipulation is pure and
WASM-portable.
---
## POC Target 1: Chunk Format + N-Channel Demux/Mux
**Question:** Does the 9-byte format decompose and recombine N concurrent
streams correctly, with per-channel order preservation and cross-channel
isolation?
**Answer:** Yes. The demux routes chunks to per-`(channel_id, stream_type)`
bounded `mpsc::Sender`s; the mux frames `Bytes` from per-channel receivers
back onto the transport. Three concurrent channels with interleaved writes
round-trip with no cross-channel contamination and per-channel order
preserved (`demux_three_concurrent_channels_no_cross_contamination`,
`mux_demux_round_trip_three_concurrent_channels`).
**Lenient unknown-`channel_id` handling (OQ-CH-12):** a chunk with an
unallocated `channel_id` (or an unallocated `stream_type` on an allocated
channel) is dropped with a debug log and an error counter, and the demux
continues (`demux_unknown_channel_drops_lenient`). This matches SSH's
behavior and survives transient mis-ordering during teardown. The error
counter is exposed via `Demux::stats()` for observability. This is the
recommended v1 behavior from the plan; the POC validates it works.
**Zero-length sentinel (TTY convention carried forward):** a zero-length
chunk is delivered as an empty `Bytes`, which `MpscRecvStream` interprets as
EOF (`demux_zero_length_sentinel_delivered`, `recv_zero_length_sentinel_is_eof`).
This is the same convention as TTY's zero-length stdin/stdout chunks. See
§"Issues Surfaced" below for a subtlety about how shutdown propagates this.
**Chunk-too-large:** a chunk with `length > MAX_CHUNK_LEN` returns
`ChunkTooLarge` and does not corrupt the stream
(`demux_chunk_too_large_does_not_panic`).
**Backpressure (DP-5 option c):** each `(channel_id, stream_type)` has an
independent bounded `mpsc` buffer. A slow reader on one channel does not block
another channel's reads — the demux's per-chunk `route` awaits the matching
sender without holding a global lock. The 1 MiB tunnel test
(`tunnel_large_payload`) exercises this end-to-end and does not deadlock.
---
## POC Target 2: Per-Channel `Connection` Presentation
**Question:** Is `Connection::from_stream` sufficient to present each channel
as a `Connection` a `ProtocolHandler` can drive unchanged?
**Answer:** Yes. The `EchoHandler` (~5 lines of handler code) runs through the
full demux→Connection→handler→mux path with zero channels-layer awareness.
It calls `connection.accept_bi().await`, gets a `(SendStream, RecvStream)`
pair, and pumps via `tokio::io::copy`. The handler does not know it is inside
a channels connection.
**Wiring (`ChannelEndpoint`):** the POC's local equivalent of the symmetric
client/server types (OQ-CH-14). `channel_endpoint()` constructs both sides of
a channels connection (server demux/mux + client demux/mux, connected by two
`tokio::io::duplex` pipes). `open_echo_channel(channel_id)` allocates a
channel on both sides: the server side wraps `MpscSendStream`/`MpscRecvStream`
as `Connection::from_stream(..., b"alknet/echo", None)` and spawns
`EchoHandler::handle` with an `AuthContext::anonymous(b"alknet/echo")`;
the client side returns a `ClientChannel { send, recv }` the test drives. This
is the POC-local shape Phase 1 will generalize into `ChannelsAdapter::handle`
(server) and `ChannelClient` (client).
**ALPN threading:** `handler_sees_alpn` validates that `Connection::from_stream`'s
`alpn` parameter threads through correctly — the handler's
`connection.remote_alpn()` returns the ALPN the channel was opened with.
**Yield-once contract:** `handler_loops_accept_bi_gets_one_session` validates
that a handler that loops `accept_bi` (like `TtyAdapter`) gets exactly one
session per channel, then `ConnectionClosed` on the second call. The
yield-once `Connection::from_stream` path composes correctly for looping
handlers.
---
## POC Target 3: Tunnel Handler
**Question:** Does the bidirectional pump pattern (same shape as TTY's
`pump_session`, two pumps instead of three) work as a generic port proxy
through channels?
**Answer:** Yes. The `TunnelHandler` (~15 lines of handler code) opens a
`TcpStream` to a fixed target and runs two `tokio::io::copy` pumps:
`recv → tcp_write` (client → target) and `tcp_read → send` (target → client).
The handler contains zero chunk-format or channels-layer awareness — it
only sees `Connection`/`SendStream`/`RecvStream`.
**Shutdown-on-completion (subtlety):** the naive `tokio::try_join!(c2t, t2c)`
deadlocks — `t2c` waits forever for the TCP echo server to close, which only
happens once the handler closes `tcp_write`, which only happens after
`try_join` returns. The POC fixes this by shutting down the opposite sink
when each pump completes: `c2t` shuts down `tcp_write` on EOF, `t2c` shuts
down `send` on EOF. This is the `pump_session` shape with an explicit
shutdown-on-completion step that the TTY adapter doesn't need (because TTY's
three pumps coordinate via the exit-code future). The tunnel's two-pump
shape needs it. See §"Issues Surfaced" below.
**Tests:**
- `tunnel_echo_round_trip` — 1 KiB round-trips through the tunnel to a TCP
echo server.
- `tunnel_large_payload` — 1 MiB round-trips without corruption or deadlock,
exercising the bounded-buffer backpressure path (the tunnel handler's
`AsyncRead` side and the channels demux must not deadlock when the TCP
echo server is slower than the channel writer).
- `tunnel_concurrent_with_echo_channel` — one tunnel channel and one echo
channel on the same channels connection, both running concurrently, neither
blocks the other. The multiplexing is transparent.
---
## Issues Surfaced (For the Spec and the Core Refactor)
These are the things the POC ran into that should be addressed in Phase 1 or
in the light `alknet-core` refactor landing alongside it. They are grouped by
where the fix likely lives.
> **Update 2026-07-12 (post-refactor):** issues #1, #2, and #3 below are
> **resolved** by ADR-070 (`BidiStreamSource` trait) and commit `60cce22`
> (`refactor(core): implement BidiStreamSource trait + AuthContext::anonymous`).
> The POC was re-verified against the updated core: all 28 tests still pass,
> clippy is now fully clean (the upstream `Connection::close` unused-arg
> warnings are gone), and the POC's handler tests were updated to use the new
> `AuthContext::anonymous(alpn)` constructor (removing the four-`None`-field
> literal that recurred across `echo_handler.rs` and `tunnel_handler.rs`).
> The POC still uses `Connection::from_stream` (the yield-once path) — see
> the note on #1 below for why that remains the correct POC scope.
### In `alknet-core` (the refactor)
#### 1. `Connection::from_stream` is yield-once — `BidiStreamSource` would be cleaner (OQ-CH-13, confirmed +EV) — ✅ RESOLVED by ADR-070
The POC validates the yield-once path is sufficient, as the plan predicted.
But wiring `ChannelEndpoint::open_echo_channel` made the awkwardness concrete:
each channel constructs a fresh `Connection::from_stream` that yields one bidi
stream, and the endpoint holds a bag of these connections (one per channel)
rather than a single `ChannelConnection` that yields N streams. The
`BidiStreamSource` trait proposed in OQ-CH-13 would make `ChannelConnection`
a first-class peer of QUIC (many bidi streams) instead of a bag of yield-once
connections.
**Resolved.** ADR-070 landed the `BidiStreamSource` trait
(`crates/alknet-core/src/types.rs:364`) with `accept_bi`/`open_bi`/
`remote_addr`/`close`, and `Connection` now holds `Box<dyn BidiStreamSource>`.
`Connection` is open for extension: the channels crate (Phase 1) will
implement `ChannelBidiStreamSource` in its own crate, without a core edit.
The `StreamBidiStreamSource` (yield-once) impl is the compatibility path —
existing callers and this POC keep working via `Connection::from_stream`,
which now wraps a `StreamBidiStreamSource`.
> **POC scope note:** the POC keeps `Connection::from_stream` (the yield-once
> path) rather than implementing `BidiStreamSource` directly. This was the
> correct POC scope — the POC's job was to validate the yield-once path is
> *sufficient* (it is) and surface the +EV refactor (ADR-070 confirms it).
> Building a `ChannelConnection` that yields N streams is Phase 1's job, now
> unblocked. ADR-070's constructor table originally implied a public
> `Connection::from_source(impl BidiStreamSource)` constructor for downstream
> crates that wasn't yet exposed; that gap was surfaced by this POC and has
> since been closed by commit `e8bbc74` (`pub fn from_source(source: impl
> BidiStreamSource, alpn: Vec<u8>)` at `types.rs:574`). The POC was
> deliberately **not retrofitted** to use `from_source` + a
> `ChannelBidiStreamSource`: the POC's objective (de-risk the three
> high-leverage unknowns, surface the +EV changes) was reached, the issues it
> surfaced are resolved, and rewiring the POC to the N-stream shape now would
> be rework that the Phase 1 crate will do authoritatively anyway. The POC
> stands as the de-risk artifact; Phase 1 builds on the now-unblocked trait
> and constructor.
#### 2. `Connection::close` has unused params (`code`, `reason`) for the stream backend — ✅ RESOLVED by ADR-070 (REQ-CORE-02)
`crates/alknet-core/src/types.rs:500` — the `Stream` backend of `Connection::close`
took `code: u32, reason: &str` and used neither (clippy flagged both as
unused). The stream backend just dropped the inner stream. This was a leak of
the QUIC-centric `close(code, reason)` shape into a backend that has no such
concept.
**Resolved.** ADR-070 §REQ-CORE-02 chose option (b): keep the QUIC-shaped
signature on the trait (so `Connection::close(code, reason)` callers are
unchanged), and the `StreamBidiStreamSource::close` impl prefixes the args
with `_code`/`_reason` and documents why they're ignored ("the drop is the
close — ADR-065 §Negative"). The clippy warning under
`--no-default-features` (the POC's build mode) is gone — verified by
`cargo clippy -p alknet-core --no-default-features` returning clean.
#### 3. `AuthContext` construction is verbose for tests and POCs — ✅ RESOLVED (REQ-CORE-03)
Every POC handler test constructed an `AuthContext` literal with four `None`
fields and a hardcoded ALPN. The real assembly layer gets one from the
endpoint; POCs and tests repeated the boilerplate. A
`AuthContext::anonymous(alpn)` helper in `alknet-core` would remove this.
**Resolved.** `crates/alknet-core/src/auth.rs:90` adds
`pub fn anonymous(alpn: impl Into<Vec<u8>>) -> Self` (no `test-utils` feature
gate — it's a plain `pub fn` useful for any caller that constructs an
`AuthContext` outside the endpoint's resolution path). The POC's
`echo_handler.rs` and `tunnel_handler.rs` were updated to use it, replacing
three four-`None`-field literals with `AuthContext::anonymous(b"alknet/echo")`
/ `AuthContext::anonymous(b"alknet/tunnel")`.
### In `alknet-channels` (the spec / Phase 1)
#### 4. `MpscSendStream::poll_shutdown` must emit a zero-length sentinel
The POC's `MpscSendStream::poll_shutdown` sends an empty `Bytes` (the EOF
sentinel) before dropping the sender. Without this, the demux never sees EOF
on the channel's `stream_type`, and `tokio::io::copy` in the handler never
completes — the test hangs. The TTY crate's `pump_session` emits the
zero-length stdout sentinel explicitly via `Chunk::stdout(Bytes::new())`;
the channels layer's per-channel write pump does NOT forward a sentinel on
sender-drop, so the send adapter must do it.
**Implication for Phase 1:** the `ChannelConnection` write-half contract
must specify that `AsyncWrite::shutdown` (or the `BidiStreamSource`
equivalent) emits a zero-length sentinel so the peer sees EOF. This is a
wire-level invariant, not just an implementation detail — both sides must
agree, or channels will hang on clean shutdown. The TTY crate already
follows this convention; the channels crate should codify it.
#### 5. The mux needs dynamic registration (handle/runner split)
The plan's `Mux::run(self, transport)` shape (consume the mux, run pumps for
pre-registered channels) does not compose with the "open a channel after the
run loop started" API that `ChannelEndpoint::open_echo_channel` needs. The
POC split `Mux` into `MuxHandle` (clone-able, `register(channel_id,
stream_type) -> Sender<Bytes>` at any time) and `MuxRunner` (owns the
transport, `select!`s on new-pump registrations). This is the shape Phase 1
should adopt — the alternative (pre-registering all channels before run)
doesn't match the dynamic `channel/open` model.
The split adds one `mpsc::UnboundedSender` + `Arc<Mutex<HashMap>>` per mux,
which is cheap. The runner's `select!` loop exits when all `MuxHandle` clones
drop (the `new_pumps` sender closes), which is the natural shutdown signal.
#### 6. The demux must drop all channel senders on transport EOF
`Demux::run` clears its `channels` map on exit (transport closed) so every
handler's `MpscRecvStream` sees EOF even without an explicit zero-length
sentinel arriving on the wire. Without this, `read_to_end` / `tokio::io::copy`
in handlers hangs forever waiting for a sender that never drops because the
demux task is holding the map. This is a teardown invariant: transport close
→ all channel senders drop → all handlers see EOF → all handler tasks exit.
Phase 1 should specify this as part of the `ChannelsAdapter::handle` contract.
#### 7. The tunnel's two-pump shape needs explicit shutdown-on-completion
`tokio::try_join!(c2t, t2c)` deadlocks for the tunnel: each pump waits for the
other's EOF, which only comes once the *opposite* pump completes and shuts
down its sink. The fix (shut down the peer's sink when one pump completes) is
small but easy to get wrong. The TTY adapter doesn't have this problem
because its three pumps coordinate via the `exit_code` future, which is a
third signal. The tunnel has no such signal — it's purely pump-driven.
**Implication for Phase 1:** the tunnel handler (and any future handler
with a pump-driven two-direction shape) must shut down the opposite sink on
pump completion, not just `try_join` the two pumps. This is worth a
documented pattern in the channels spec, or — now that `BidiStreamSource`
has landed (ADR-070) — a helper in `alknet-core` that encapsulates the
"two-pump with shutdown-on-completion" shape so handlers don't reimplement
it. The TTY crate's `pump_session` is the three-pump version; the two-pump
version is the tunnel's shape and will recur (e.g. an SSH `direct-tcpip`
channel is the same two-pump shape).
### Cosmetic / clippy
#### 8. "Very complex type" for the `reserve_owned` future
`MpscSendStream` stores an in-flight `reserve_owned()` future for
backpressure. The concrete future type is anonymous (async fn), so it's
boxed as `Pin<Box<dyn Future<Output = Result<OwnedPermit<Bytes>, SendError<()>>> + Send>>`.
Clippy flags this as "very complex type." The POC factored it into a
`type ReserveFut = ...` alias, which silences clippy but doesn't reduce the
complexity. The clean fix is `tokio_util::io::PollSender`-style helper that
wraps an `mpsc::Sender<T>` as `AsyncWrite` for `T: Into<Bytes>` — but that
requires `tokio-util` as a dep and a small adapter. Phase 1 should consider
whether to extract this adapter (it's ~50 lines and will recur in any
mpsc-backed stream) or keep it POC-local. Low priority.
---
## What the POC Does NOT Validate
Following the docker POC summary's pattern:
1. **The call protocol.** `channel/open`, `channel/close`, `channel/control`,
`channel/resources` are call-protocol operations. The POC uses a POC-local
channel-allocation mechanism (`Demux::allocate_channel` /
`MuxHandle::register`), not the real `OperationRegistry` path. The call
protocol is already in production; its integration with channels is Phase 1's
concern, not a POC unknown.
2. **Real transport.** `tokio::io::duplex` stands in for QUIC/TCP/WebTransport.
The transport-generalization work already validated `Connection::from_stream`
over real transports; the POC reuses that.
3. **ACL.** The call protocol's `AccessControl::check` gates `channel/open`
in the real system; the POC has no auth.
4. **Real adapters.** `EchoHandler` and `TunnelHandler` are POC-local stubs.
The real `TtyAdapter` / `SshAdapter` / `DockerTtyBackend` are unchanged and
integrate in Phase 1.
5. **Recursive composition.** `alknet/channels` inside `alknet/channels` is a
natural consequence of the `Connection` abstraction but is not a POC goal.
6. **Hub relay.** The stretch goal (two `Demux`/`Mux` pairs bridged by a byte
pump with `channel_id` remapping, OQ-CH-11) was not built — the three primary
targets were the higher-leverage unknowns. The POC's demux/mux shape would
support it, but the relay sketch is left for Phase 1.
7. **WASM test harness.** The sync core compiles under
`wasm32-unknown-unknown`; running the same tests in a WASM test harness
(`wasm-bindgen-test`) was the secondary stretch and was not set up. The
compile check validates the "no platform dependencies" claim for the core;
the async shell and `alknet-core` dep graph are not WASM-clean yet (transitive
`getrandom`/`rand`), which is a Phase 1 concern.
---
## Test Coverage
```
running 28 tests
test wire::tests::parse_max_length ... ok
test wire::tests::parse_preserves_channel_id_max ... ok
test wire::tests::parse_round_trip ... ok
test wire::tests::parse_too_large ... ok
test wire::tests::parse_zero_length ... ok
test mpsc_stream::tests::recv_dropped_sender_is_eof ... ok
test mpsc_stream::tests::recv_partial_then_leftover ... ok
test mpsc_stream::tests::recv_reads_bytes_in_order ... ok
test mpsc_stream::tests::recv_zero_length_sentinel_is_eof ... ok
test mpsc_stream::tests::send_after_close_errors_broken_pipe ... ok
test mpsc_stream::tests::send_backpressure_pends_then_completes ... ok
test mpsc_stream::tests::send_writes_then_shutdown_closes ... ok
test demux::tests::demux_chunk_too_large_does_not_panic ... ok
test demux::tests::demux_routes_to_allocated_channel ... ok
test demux::tests::demux_three_concurrent_channels_no_cross_contamination ... ok
test demux::tests::demux_unknown_channel_drops_lenient ... ok
test demux::tests::demux_zero_length_sentinel_delivered ... ok
test mux::tests::mux_demux_round_trip_three_concurrent_channels ... ok
test mux::tests::mux_frames_single_chunk_round_trip ... ok
test mux::tests::mux_zero_length_sentinel_round_trips ... ok
test echo_handler::tests::echo_single_channel ... ok
test echo_handler::tests::echo_single_channel_1kib ... ok
test echo_handler::tests::echo_three_concurrent_channels ... ok
test echo_handler::tests::handler_loops_accept_bi_gets_one_session ... ok
test echo_handler::tests::handler_sees_alpn ... ok
test tunnel_handler::tests::tunnel_concurrent_with_echo_channel ... ok
test tunnel_handler::tests::tunnel_echo_round_trip ... ok
test tunnel_handler::tests::tunnel_large_payload ... ok
test result: ok. 28 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
```
The 1 MiB `tunnel_large_payload` test exercises the bounded-buffer
backpressure path end-to-end (channel writer faster than TCP echo server,
no deadlock). The three `echo_*` tests validate the full
demux→Connection→handler→mux path. The five `wire::*` tests are pure sync
core, WASM-portable.
---
## POC Structure
```
alknet-channels-poc/
Cargo.toml — depends on alknet-core (path, no default features), tokio, tokio-util, bytes, async-trait, thiserror, tracing
src/
lib.rs — module docs; the three-step POC overview
wire.rs — sync core: 9-byte ChunkHeader, parse_header, write_header (WASM-clean)
mpsc_stream.rs — MpscRecvStream (AsyncRead), MpscSendStream (AsyncWrite + backpressure + sentinel-on-shutdown)
demux.rs — Demux: route chunks to per-(channel,stream_type) bounded mpsc senders; lenient on unknown ids; drop senders on EOF
mux.rs — MuxHandle/MuxRunner: dynamic per-channel write pumps; frames Bytes onto the shared transport
echo_handler.rs — EchoHandler (ProtocolHandler) + ChannelEndpoint (POC-local client/server wiring) + open_echo_channel
tunnel_handler.rs — TunnelHandler (two-pump TcpStream proxy) + tcp echo server test helper
```
No `tests/` directory — tests are inline per-module (`#[cfg(test)] mod tests`),
matching the alknet-core / alknet-tty convention (vs. the docker POC's
separate `tests/integration.rs`, which matched bollard's style).
---
## Key Code-to-Concept Mappings
| POC concept | alknet-core equivalent | alknet-tty equivalent |
|---|---|---|
| `wire::parse_header`/`write_header` | — | `wire::ChunkReader::read_chunk` / `ChunkWriter::write_chunk` (5-byte, no `channel_id`) |
| `MpscRecvStream` (`AsyncRead`) | backs `RecvStream::from_stream` | the channel-bridge adapter in the TTY POC |
| `MpscSendStream` (`AsyncWrite`) | backs `SendStream::from_stream` | `TestStdinSink` in TTY adapter tests |
| `Demux` | — | (new — TTY has no per-channel demux; it's single-channel) |
| `MuxHandle`/`MuxRunner` | — | (new — TTY has `ChunkWriter` directly, no mux) |
| `ChannelEndpoint` | the symmetric client/server types (OQ-CH-14) | — |
| `Connection::from_stream(send, recv, alpn, addr)` | `types.rs:405` | `TtyAdapter::handle` receives a `Connection` the same way |
| `EchoHandler::handle` | `ProtocolHandler::handle` | `TtyAdapter::handle` (loops `accept_bi`) |
| `TunnelHandler::handle` two pumps | — | `pump_session` (three pumps — stdout/stderr + client→backend + exit) |
| zero-length sentinel = EOF | — | `wire.rs` §Sentinels (ADR-052) |
---
## References
- POC plan: `docs/research/alknet-channels/poc-plan.md` — the detailed
de-risk POC plan this summary reports against.
- Phase 0 findings: `docs/research/alknet-channels/phase-0-findings.md` — the
research doc this POC derisks (OQ-CH-12/13/14 all surfaced there).
- TTY wire format: `crates/alknet-tty/src/wire.rs` — the 5-byte
`ChunkReader`/`ChunkWriter` this POC generalizes to 9 bytes.
- TTY adapter: `crates/alknet-tty/src/adapter.rs` — `TtyAdapter::handle` and
`drive_session`/`pump_session` (the per-stream dispatch and three-pump
bidirectional pattern the tunnel handler reuses with two pumps).
- Core types: `crates/alknet-core/src/types.rs` — `Connection::from_stream`
(`:541`), `Connection::from_source` (`:574`, ADR-070's downstream extension
point — `impl BidiStreamSource` → `Connection`, no core edit), `SendStream::
from_stream` (`:267`), `RecvStream::from_stream` (`:289`), `ProtocolHandler`
(`:220`), `BidiStreamSource` trait (`:364`, ADR-070) — the integration
points the POC validates. The POC uses `Connection::from_stream`
(yield-once); the channels crate (Phase 1) will use `from_source` with its
own `ChannelBidiStreamSource` impl.
- Core auth: `crates/alknet-core/src/auth.rs` — `AuthContext::anonymous`
(`:90`, REQ-CORE-03), used by the POC's echo and tunnel handler tests.
- ADR-070: `docs/architecture/decisions/070-bidistreamsource-trait.md` — the
`BidiStreamSource` trait decision this POC's findings prompted (resolves
Issues #1 and #2 below; REQ-CORE-01/02).
- ADR-065: `docs/architecture/decisions/065-connection-from-stream-generic-single-stream.md`
— the `Connection::from_stream` yield-once contract the POC validated and
the `StreamBidiStreamSource` impl now preserves.
- Docker POC summary: `docs/research/alknet-docker/poc-summary.md` — the
POC summary this doc mirrors in structure and tone.
- TTY POC: `/workspace/alknet-tty-poc/` — the POC convention this POC follows
(sibling-of-`@alkdev` location, standalone Cargo crate, inline tests).