From 5e07203b869c593a008e7df97f9e64ebadb3b05b Mon Sep 17 00:00:00 2001 From: "glm-5.2" Date: Sun, 12 Jul 2026 06:35:50 +0000 Subject: [PATCH] docs(research): add detailed channels POC plan; add BidiStreamSource + client endpoint open questions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds docs/research/alknet-channels/poc-plan.md — a standalone three-step POC plan that derisks the channels layer in isolation (no call protocol, no real transport, no real adapters): - Step 1: chunk format + N-channel demux/mux — generalizes TTY's 5-byte ChunkReader/ChunkWriter to the 9-byte format, validates decompose→stream→ recompose for 3+ concurrent channels with the sync-core/async-shell split pattern from TTY's REQ-TTY-01. - Step 2: per-channel Connection presentation — wraps each reassembled channel as Connection::from_stream and runs a minimal echo ProtocolHandler through the full path. Validates the existing Connection abstraction is sufficient; no core changes needed for the POC. - Step 3: tunnel handler — opens a TcpStream and pumps bidirectionally, reusing TTY's pump_session shape with two pumps. Validates the same concepts behind the TTY crate work as a generic port proxy. Stretch goals: WASM build of the sync core, mixed channel types on one connection, hub relay sketch with channel_id remapping. Adds three open questions to phase-0-findings.md: - OQ-CH-12: unknown channel_id on demux (lenient drop vs strict error). - OQ-CH-13: core BidiStreamSource trait — additive refactor to make ChannelConnection a first-class peer of QUIC (many bidi streams) rather than a bag of yield-once Connections. Likely +EV; not needed for the POC but should be evaluated in Phase 1. - OQ-CH-14: client-side channels endpoint — the symmetric ChannelClient type (analogue of AlknetEndpoint vs CallClient). After the POC we're probably going to need some light refactoring to the core to make these easier; mostly additive and not breaking in major/pita ways. Updates phase-0-findings.md De-risk POC section to point at the detailed plan and adds the poc-plan to References. --- .../alknet-channels/phase-0-findings.md | 100 +++- docs/research/alknet-channels/poc-plan.md | 493 ++++++++++++++++++ 2 files changed, 576 insertions(+), 17 deletions(-) create mode 100644 docs/research/alknet-channels/poc-plan.md diff --git a/docs/research/alknet-channels/phase-0-findings.md b/docs/research/alknet-channels/phase-0-findings.md index 45709c4..41fdbe5 100644 --- a/docs/research/alknet-channels/phase-0-findings.md +++ b/docs/research/alknet-channels/phase-0-findings.md @@ -1427,6 +1427,55 @@ same as any multiplexer. side. Phase 1 must specify whether the hub translates or transparently forwards, and how the `channel_id` mapping is maintained. +- **OQ-CH-12 (unknown channel_id on demux)**: when the demux receives a + chunk with a `channel_id` it has not allocated, what does it do? Options: + (a) drop with a debug log (lenient — survives transient mis-ordering + during channel teardown), (b) return a protocol error and close the + transport (strict — catches bugs but is fragile during teardown). SSH + is lenient. Recommendation: lenient for v1, with an error counter for + observability. See `poc-plan.md` §Step 1. + +- **OQ-CH-13 (core trait for bidi-stream sources)**: the POC + (`poc-plan.md`) uses `Connection::from_stream` per channel, which is + yield-once. A Phase 1 refactor may add a `BidiStreamSource` trait to + `alknet-core` so `ChannelConnection` (many channels, each a bidi stream) + is a first-class peer of QUIC (many bidi streams) rather than a bag of + yield-once Connections: + + ```rust + trait BidiStreamSource: Send + Sync { + async fn accept_bi(&self) -> Result<(SendStream, RecvStream), StreamError>; + async fn open_bi(&self) -> Result<(SendStream, RecvStream), StreamError>; + } + ``` + + with `Connection` holding `Box`, and QUIC/Iroh/ + Stream/Channels all implementing it. This is **additive** (existing + callers keep working via a blanket impl or a `from_stream`-backed + default) and not a major/pita break — it mostly adds a new variant and + trait-ifies the existing `accept_bi`/`open_bi` methods behind a trait + object. The POC does NOT need this — it validates the yield-once path is + sufficient — but Phase 1 should evaluate whether the trait makes the + channels layer and the client-side endpoint (OQ-CH-14) cleaner. The + expectation is that this route is +EV: it's not a massive change and + will make things a lot easier downstream. + +- **OQ-CH-14 (client-side channels endpoint)**: both sides of a channels + connection do the demux/mux work. The server side is a `ProtocolHandler` + (`ChannelsAdapter::handle`). The client side needs a symmetric type — + something like `ChannelClient` that opens a transport, runs the demux/ + mux, and exposes `open_channel(alpn, params) -> Channel` to the + application. This is the channels analogue of `AlknetEndpoint` (server) + vs `CallClient` (client) in the call protocol. The POC uses a POC-local + client type; Phase 1 must decide whether this lives in `alknet-channels` + or is a thin wrapper over `alknet-core`'s endpoint types. The + `BidiStreamSource` trait (OQ-CH-13) may factor into this — if + `AlknetEndpoint` and `ChannelClient` both produce `BidiStreamSource`s, + the client/server symmetry is cleaner. After the POC we're probably + going to need some light refactoring to the core to make these easier; + the good news is it should mostly be additive and not breaking in + major/pita ways. + ## Recommended Approach ### Crate @@ -1485,29 +1534,46 @@ on `alknet-channels` except the assembly layer that registers it on the ### De-risk POC -A Phase 0 POC should validate the core claim: **one connection, multiple -channel types, orchestrated by the call protocol.** Minimum scope: +A detailed POC plan lives in `docs/research/alknet-channels/poc-plan.md`. +Summary: a standalone POC at `/workspace/@alkdev/alknet-channels-poc/` that +validates the three highest-leverage unknowns in three independently-runnable +steps: -1. Chunk format round-trip with 3+ concurrent channels -2. Channel open/close via call protocol operations -3. TTY session inside a channel (reuse the existing `alknet-tty-poc` - test infrastructure) -4. Bidirectional byte pumping on a data channel +1. **Chunk format + N-channel demux/mux** — generalize TTY's 5-byte + `ChunkReader`/`ChunkWriter` to the 9-byte format, build a demux that + routes chunks to per-channel `mpsc` channels and a mux that frames + per-channel bytes back onto the transport. Validates the + decompose→stream→recompose round-trip for 3+ concurrent channels. +2. **Per-channel `Connection` presentation** — wrap each reassembled + channel as `Connection::from_stream` and run a minimal `ProtocolHandler` + (echo) through the full demux→Connection→handler→mux path. Validates + that the existing `Connection` abstraction (from transport-generalization) + is sufficient; no core changes needed for the POC. +3. **Tunnel handler** — a `channel/open` with a target address opens a + `TcpStream` and pumps bidirectionally. Validates that the same concepts + behind the TTY crate work as a generic port proxy. -Stretch goals: -5. Two different channel types (TTY + tunnel) on the same connection -6. WASM build of the chunk splitting/recombining logic -7. Hub relay: two `ChannelManager` instances bridged by a byte pump, - validating that `channel/open` on leg A translates to `channel/open` on - leg B and the `channel_id` remapping works end-to-end (OQ-CH-11) -8. `channel/resources` discovery — one side lists its exposed ALPNs and the - other opens a channel based on the response +Stretch goals: WASM build of the sync core; two different channel types +(TTY-shaped 4-stream + tunnel 2-stream) on one connection; hub relay sketch +with `channel_id` remapping (OQ-CH-11). -The POC can be built as an extension to the existing `alknet-tty-poc` or as -a standalone POC in `/workspace/alknet-channels-poc/`. +The POC deliberately does NOT do: the call protocol (channel/open etc. are +Phase 1's concern, already in production), real transport (uses +`tokio::io::duplex`), ACL, real adapters, or recursive composition. + +The POC surfaces three open questions carried into Phase 1: OQ-CH-12 +(unknown channel_id on demux), OQ-CH-13 (core `BidiStreamSource` trait — +likely +EV additive refactor after the POC), and OQ-CH-14 (client-side +channels endpoint — the symmetric `ChannelClient` type). + +The POC lives at `/workspace/@alkdev/alknet-channels-poc/` (mirroring the +`alknet-tty-poc` convention), depends on `alknet-core` only. ## References +- `docs/research/alknet-channels/poc-plan.md` — the detailed de-risk POC + plan (Step 1: chunk format + demux/mux, Step 2: per-channel Connection, + Step 3: tunnel handler). - `docs/research/alknet-tty/phase-0-findings.md` — the TTY crate's chunk format, control channel, and backend trait. The seed of the channels generalization. diff --git a/docs/research/alknet-channels/poc-plan.md b/docs/research/alknet-channels/poc-plan.md new file mode 100644 index 0000000..e0b5601 --- /dev/null +++ b/docs/research/alknet-channels/poc-plan.md @@ -0,0 +1,493 @@ +--- +status: plan +last_updated: 2026-07-12 +--- + +# alknet-channels: De-Risk POC Plan + +**Status:** Plan. The POC has not been built. This document specifies what to +build, in what order, and what each step must demonstrate before proceeding. +It is the concrete answer to OQ-CH-07 in `phase-0-findings.md`. +**Date:** 2026-07-12 +**Scope:** A standalone POC that validates the three highest-leverage +unknowns of the channels layer: (1) the 9-byte chunk format decomposes and +recombines N concurrent streams correctly, (2) each reassembled channel +presents as a `Connection` that a `ProtocolHandler` can drive unchanged, +(3) the same shape works as a tunnel proxy for a local TCP port. The POC +deliberately avoids the call protocol and the real adapters — it isolates +the channels layer's core mechanics. + +--- + +## Executive Summary + +The POC is structured as three independently-runnable steps, each building +on the previous, each with a clear pass/fail criterion. All three run over +a `tokio::io::duplex` stand-in transport (no QUIC, no TLS) to keep the POC +self-contained and WASM-buildable. + +1. **Chunk format + N-channel demux/mux** — generalize TTY's + `ChunkReader`/`ChunkWriter` to the 9-byte format, build a demux that + routes chunks to per-channel `mpsc` channels and a mux that frames + per-channel bytes back onto the transport. Validates the + decompose→stream→recompose round-trip for 3+ concurrent channels. +2. **Per-channel `Connection` presentation** — wrap each reassembled + channel as `Connection::from_stream(send, recv, alpn, addr)` and run a + minimal `ProtocolHandler` (echo) through the full demux→Connection→ + handler→mux path. Validates that the existing `Connection` abstraction + (landed in the transport-generalization work) is sufficient; no core + changes needed for the POC. +3. **Tunnel handler** — a `channel/open` with a target address opens a + `TcpStream` and pumps bidirectionally between the channel and the TCP + socket. Validates that the bidirectional pump pattern (same as TTY's + `pump_session`) works as a generic port proxy and that the handler sees + a `Connection` without knowing it's inside channels. + +**Stretch goal:** run the demux/mux sync core under wasm32-unknown-unknown +to validate the "no platform dependencies" claim (WASM compatibility by +construction). + +The POC lives at `/workspace/@alkdev/alknet-channels-poc/` (mirroring the +`alknet-tty-poc` convention), depends on `alknet-core` for `Connection`/ +`SendStream`/`RecvStream`/`ProtocolHandler`, and does NOT depend on +`alknet-tty`, `alknet-call`, or any other handler crate. The echo handler +(Step 2) and tunnel handler (Step 3) are POC-local stubs, not the real +adapters. + +--- + +## Design Principles (carried from the TTY POC) + +### The sync core / async shell split + +The TTY POC's REQ-TTY-01 insight (`docs/research/alknet-tty/phase-0-findings.md` +§REQ-TTY-01) was that `portable_pty` is blocking `std::io`, and the POC +bridged it via std threads + tokio mpsc. The channels demux has an analogous +split, but simpler — the "sync" part is pure byte manipulation, not +blocking I/O: + +- **Sync core (WASM-compatible, no async)**: `parse_chunk_header(&[u8; 9]) + -> (channel_id, stream_type, length)` and `frame_chunk(channel_id, + stream_type, payload) -> Vec`. Pure functions. These are the + generalization of TTY's `ChunkReader::read_chunk` / `ChunkWriter:: + write_chunk` with the `channel_id` field added. +- **Async shell**: one `read_exact` on the transport to get the 9-byte + header, one `read_exact` for the payload, then route the payload to the + right per-channel `mpsc::Sender`. That's the demux loop — one + async task per transport. +- **Per-channel reassembly**: `mpsc::Receiver` wrapped as + `AsyncRead` (small adapter, ~30 lines), fed to the handler as a + `RecvStream` via `RecvStream::from_stream`. The handler's writes go to + an `mpsc::Sender` wrapped as `AsyncWrite`, which the channels + layer's per-channel write pump reads from and frames back onto the + transport. + +The channels layer is structured exactly like the TTY POC's bridge: sync +byte manipulation at the core, async shell around it, mpsc channels +connecting the two worlds. The POC validates this is as clean as it was +for TTY. + +### Streams are streams + +The guiding insight from `phase-0-findings.md`: a TTY session, an SSH +channel, a forwarded TCP connection — they're all just +`AsyncRead + AsyncWrite` handles. The differences are only in how they're +*opened* and what *multiplexing layer* carries them. The POC validates the +multiplexing-layer half; the open-negotiation half is validated by the call +protocol (already in production) and is out of scope for this POC. + +### The tunnel reuses the TTY shape, trivially + +A tunnel handler receives a channel (a `Connection::from_stream` yielding +one bidi stream), calls `accept_bi()`, gets the `SendStream`/`RecvStream` +pair, opens a `TcpStream` to the local port, and runs two pumps: + +- `RecvStream` → `TcpStream` write half (client → target) +- `TcpStream` read half → `SendStream` (target → client) + +That's the TTY `pump_session` pattern with two pumps instead of three (no +control, no stderr). The chunk format is the channels layer's concern, not +the tunnel handler's — the handler just sees opaque bytes on its +`SendStream`/`RecvStream`. `TcpStream` is natively async (tokio), so no +blocking→async bridge is needed for the tunnel itself. The "don't have to +async" pattern lives in the channels demux, not in the tunnel handler. + +--- + +## Step 1: Chunk Format + N-Channel Demux/Mux + +**Goal:** validate the decompose→stream→recompose round-trip for N +concurrent channels. + +### What to build + +```rust +// wire.rs — pure sync core, no async, no platform deps + +const CHUNK_HEADER_LEN: usize = 9; // channel_id:u32 + stream_type:u8 + length:u32 +const MAX_CHUNK_LEN: u32 = 16 * 1024 * 1024; + +pub struct ChunkHeader { + pub channel_id: u32, + pub stream_type: u8, + pub length: u32, +} + +pub fn parse_header(buf: &[u8; 9]) -> Result { ... } +pub fn write_header(channel_id: u32, stream_type: u8, length: u32, out: &mut [u8; 9]) { ... } + +// demux.rs — async shell, one task per transport + +pub struct Demux { + // channel_id → (tx per stream_type) + channels: HashMap>>, +} + +impl Demux { + pub async fn run(&self, transport: R) { ... } +} + +pub struct Mux { + // reads from per-channel mpsc::Receiver, frames chunks onto transport +} + +impl Mux { + pub async fn run(&self, transport: W) { ... } +} +``` + +The `Demux` reads 9-byte headers, looks up `(channel_id, stream_type)` in +its map, and sends the payload to the matching `mpsc::Sender`. The `Mux` +reads from per-channel `mpsc::Receiver`s and writes framed chunks onto the +transport. Both sides use `tokio::io::duplex` as the stand-in transport. + +Per-channel reassembly: `mpsc::Receiver` wrapped as `AsyncRead` +(`MpscRecvStream` — ~30 lines, mirrors the TTY POC's channel-bridge +adapter). The write side: `mpsc::Sender` wrapped as `AsyncWrite` +(`MpscSendStream` — ~30 lines). These are the POC's `RecvStream`/`SendStream` +backings, constructed via `RecvStream::from_stream` / `SendStream::from_stream`. + +### Tests + +- `round_trip_single_channel`: one channel, write distinct data on each + `stream_type`, read back in order, assert integrity. +- `round_trip_three_concurrent_channels`: three channels, each with + different `stream_type` sets, concurrent writers and readers, assert no + cross-channel contamination and per-channel order preservation. +- `zero_length_sentinel`: a zero-length chunk is delivered as an empty + `Bytes` (the EOF sentinel, same as TTY). +- `chunk_too_large`: a chunk with `length > MAX_CHUNK_LEN` returns + `ChunkTooLarge`, doesn't corrupt the stream. +- `unknown_channel_id`: a chunk with an unallocated `channel_id` is + dropped with a debug log (or returned as an error — decide in the POC; + see OQ-CH-12 below). +- `backpressure`: a slow reader on channel A does not block channel B's + reads (the bounded-buffer property from DP-5). + +### Pass criterion + +All six tests pass. The sync core (`parse_header`/`write_header`) compiles +under `wasm32-unknown-unknown` (stretch: run the same tests in a WASM test +harness). + +### What this validates + +- The 9-byte format is a clean generalization of TTY's 5-byte format. +- The sync core is pure and platform-independent. +- The mpsc-bridged async shell scales to N concurrent channels with + per-channel order preservation and cross-channel isolation. +- The bounded-buffer backpressure (DP-5 option c) works for the simple + case. + +### What this does NOT validate + +- `Connection` presentation (Step 2). +- Handler integration (Step 2). +- Real transport (QUIC/TCP/WebTransport). +- Call-protocol orchestration (channel/open etc.). + +--- + +## Step 2: Per-Channel `Connection` Presentation + +**Goal:** validate that the existing `Connection::from_stream` is +sufficient to present each channel as a `Connection` a `ProtocolHandler` +can drive unchanged. + +### What to build + +```rust +// echo_handler.rs — a minimal ProtocolHandler +pub struct EchoHandler; + +#[async_trait] +impl ProtocolHandler for EchoHandler { + fn alpn(&self) -> &'static [u8] { b"alknet/echo" } + + async fn handle(&self, connection: Connection, _auth: &AuthContext) + -> Result<(), HandlerError> + { + let (mut send, mut recv) = connection.accept_bi().await?; + // echo: read from recv, write to send + tokio::io::copy(&mut recv, &mut send).await?; + Ok(()) + } +} +``` + +The demux loop, on allocating a new channel, constructs a +`Connection::from_stream(send, recv, alpn, remote_addr)` — where `send` +and `recv` are the `MpscSendStream`/`MpscRecvStream` from Step 1 — and +hands it to `EchoHandler::handle`. The handler calls `accept_bi()` once +(yield-once contract), gets the stream pair, and pumps. The handler does +not know it's inside a channels connection. + +The test wires both sides: a client-side `Mux`/`Demux` pair and a +server-side `Mux`/`Demux` pair, connected by two `tokio::io::duplex` +pipes (one for each direction). The client writes to a channel's +`SendStream`; the server's `EchoHandler` reads from its `RecvStream` and +writes back to its `SendStream`; the client reads the echo from its +`RecvStream`. + +### Tests + +- `echo_single_channel`: write 1 KiB to channel 1's `SendStream`, read + the echo back from channel 1's `RecvStream`, assert equality. +- `echo_three_concurrent_channels`: three channels, each echoing + concurrently, assert each gets its own data back. +- `handler_sees_alpn`: the `Connection`'s `remote_alpn()` returns the + ALPN the channel was opened with (validates `from_stream`'s `alpn` + parameter threads through correctly). +- `handler_loops_accept_bi_gets_one_session`: a handler that loops + `accept_bi` (like `TtyAdapter`) gets exactly one session per channel, + then `ConnectionClosed` — the yield-once contract. + +### Pass criterion + +All four tests pass. The `EchoHandler` is transport-agnostic — it compiles +and runs unchanged whether the `Connection` is a real QUIC connection or a +channels reassembled stream. + +### What this validates + +- `Connection::from_stream` (landed in transport-generalization) is + sufficient for per-channel presentation. No new `ConnectionKind` is + needed for the POC. +- The yield-once contract composes correctly for handlers that loop + `accept_bi`. +- The `Connection` abstraction's `alpn` and `remote_addr` fields thread + through correctly for channels. + +### What this does NOT validate + +- The tunnel proxy (Step 3). +- Real transport. +- Multiple channels of different ALPN types on one connection (that's a + Step 3 / stretch concern). + +--- + +## Step 3: Tunnel Handler + +**Goal:** validate that the bidirectional pump pattern (same as TTY's +`pump_session`) works as a generic port proxy through channels. + +### What to build + +```rust +// tunnel_handler.rs — a ProtocolHandler that proxies to a TCP target +pub struct TunnelHandler; + +#[async_trait] +impl ProtocolHandler for TunnelHandler { + fn alpn(&self) -> &'static [u8] { b"alknet/tunnel" } + + async fn handle(&self, connection: Connection, _auth: &AuthContext) + -> Result<(), HandlerError> + { + let (mut send, mut recv) = connection.accept_bi().await?; + // In the POC, the target address is fixed (or passed via a + // POC-local mechanism — NOT the real channel/open params, which + // are a call-protocol concern and out of scope). + let target = "127.0.0.1:0"; // the test's echo server + let mut tcp = TcpStream::connect(target).await?; + // Two pumps: + let (mut tcp_read, mut tcp_write) = tcp.into_split(); + let c2t = tokio::io::copy(&mut recv, &mut tcp_write); + let t2c = tokio::io::copy(&mut tcp_read, &mut send); + tokio::try_join!(c2t, t2c)?; + Ok(()) + } +} +``` + +The test spins up a local TCP echo server (`tokio::net::TcpListener` with +a per-connection echo task), opens a channel with ALPN `alknet/tunnel`, +writes data to the channel's `SendStream`, and reads the echo back from +the channel's `RecvStream`. The tunnel handler forwards bytes between the +channel and the `TcpStream` — two `tokio::io::copy` pumps, no chunk-format +awareness, no channels-layer awareness. + +### Tests + +- `tunnel_echo_round_trip`: write 1 KiB to channel's `SendStream` → + tunnel handler forwards to TCP echo server → response flows back through + channel to `RecvStream` → assert equality. +- `tunnel_large_payload`: write 1 MiB, assert it round-trips without + corruption (exercises 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, + assert neither blocks the other. + +### Pass criterion + +All three tests pass. The `TunnelHandler` is ~15 lines of handler code and +contains zero chunk-format or channels-layer awareness — it only sees +`Connection`/`SendStream`/`RecvStream`. + +### What this validates + +- The same concepts behind the TTY crate work as a generic port proxy. +- The bidirectional pump pattern (`pump_session` shape) reuses cleanly + with two pumps instead of three. +- The handler sees a `Connection` and doesn't know it's inside channels. +- A tunnel channel and an echo channel coexist on one channels connection + without interference — the multiplexing is transparent. + +### What this does NOT validate + +- Real `channel/open` with `params: { target: "..." }` — the POC uses a + fixed target or a POC-local mechanism, not the call-protocol operation + (which is out of scope). +- TLS, SSH, or WebTransport transports. +- ACL gating (the call protocol's `AccessControl::check` — out of scope). + +--- + +## Stretch Goals + +### WASM build of the sync core + +`parse_header` / `write_header` compile under `wasm32-unknown-unknown`. +The demux/mux async shell compiles under WASM with `wasm-bindgen-futures`. +This validates the "WASM compatibility by construction" claim — the core +byte manipulation has no platform dependencies, and a browser can run a +channels client in WASM over WebTransport. + +### Two different channel types on one connection + +Step 3's `tunnel_concurrent_with_echo_channel` already does this +partially. The stretch is to run a TTY-shaped channel (4 stream_types: +0/1/2/3) alongside a tunnel channel (2 stream_types: 0/1) on the same +channels connection, validating that the `stream_types` field at open +time correctly sizes the reassembly and that mixed stream_type sets +coexist. + +### Hub relay sketch + +Two `Demux`/`Mux` pairs bridged by a byte pump, validating that a chunk +arriving on channel `N` on leg A is re-framed with `channel_id = M` on +leg B (OQ-CH-11). This is a POC-level validation of the hub relay concept +from `phase-0-findings.md` §"The hub relay: ChannelManager-to- +ChannelManager". The POC version does not run the call protocol — it just +validates the byte-pump relay works with `channel_id` remapping. + +--- + +## What the POC Deliberately Does NOT Do + +To keep scope de-risked and bounded: + +- **No call protocol.** `channel/open`, `channel/close`, + `channel/control`, `channel/resources` are call-protocol operations + (`phase-0-findings.md` §Channel Open Negotiation). The POC uses a + POC-local channel-allocation mechanism (direct `Demux::allocate_channel` + calls), 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. +- **No 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. +- **No ACL.** The call protocol's `AccessControl::check` gates + `channel/open` in the real system; the POC has no auth. +- **No real adapters.** `EchoHandler` and `TunnelHandler` are POC-local + stubs. The real `TtyAdapter` / `SshAdapter` / `DockerTtyBackend` are + unchanged and integrate in Phase 1. +- **No recursive composition.** `alknet/channels` inside + `alknet/channels` is a natural consequence of the `Connection` + abstraction but is not a POC goal. The POC validates one level of + multiplexing. + +--- + +## Open Questions Surfaced by the POC Plan + +These are added to `phase-0-findings.md` §Open Questions (OQ-CH-12, +OQ-CH-13, OQ-CH-14): + +- **OQ-CH-12 (unknown channel_id on demux)**: when the demux receives a + chunk with a `channel_id` it has not allocated, what does it do? Options: + (a) drop with a debug log (lenient — survives transient mis-ordering + during channel teardown), (b) return a protocol error and close the + transport (strict — catches bugs but is fragile during teardown). SSH + is lenient. Recommendation: lenient for v1, with an error counter for + observability. + +- **OQ-CH-13 (core trait for bidi-stream sources)**: the POC uses + `Connection::from_stream` per channel, which is yield-once. A Phase 1 + refactor may add a `BidiStreamSource` trait to `alknet-core` so + `ChannelConnection` (many channels, each a bidi stream) is a first-class + peer of QUIC (many bidi streams) rather than a bag of yield-once + Connections: + + ```rust + trait BidiStreamSource: Send + Sync { + async fn accept_bi(&self) -> Result<(SendStream, RecvStream), StreamError>; + async fn open_bi(&self) -> Result<(SendStream, RecvStream), StreamError>; + } + ``` + + with `Connection` holding `Box`, and QUIC/Iroh/ + Stream/Channels all implementing it. This is additive (existing callers + keep working via a blanket impl or a `from_stream`-backed default) and + not a major/pita break. The POC does NOT need this — it validates the + yield-once path is sufficient — but Phase 1 should evaluate whether the + trait makes the channels layer and the client-side endpoint (OQ-CH-14) + cleaner. + +- **OQ-CH-14 (client-side channels endpoint)**: both sides of a channels + connection do the demux/mux work. The server side is a `ProtocolHandler` + (`ChannelsAdapter::handle`). The client side needs a symmetric type — + something like `ChannelClient` that opens a transport, runs the demux/ + mux, and exposes `open_channel(alpn, params) -> Channel` to the + application. This is the channels analogue of `AlknetEndpoint` (server) + vs `CallClient` (client) in the call protocol. The POC uses a + POC-local client type; Phase 1 must decide whether this lives in + `alknet-channels` or is a thin wrapper over `alknet-core`'s endpoint + types. The `BidiStreamSource` trait (OQ-CH-13) may factor into this — + if `AlknetEndpoint` and `ChannelClient` both produce `BidiStreamSource`s, + the client/server symmetry is cleaner. + +--- + +## References + +- `docs/research/alknet-channels/phase-0-findings.md` — the research doc + this POC derisks. +- `docs/research/alknet-tty/phase-0-findings.md` §REQ-TTY-01 — the sync + core / async shell split pattern this POC generalizes. +- `docs/research/transport-generalization/findings.md` — the + `Connection::from_stream` / `Connection::from_bidi` work this POC + builds on. +- `crates/alknet-tty/src/wire.rs` — the 5-byte chunk format + (`ChunkReader`/`ChunkWriter`) this POC generalizes to 9 bytes. +- `crates/alknet-tty/src/adapter.rs` — `TtyAdapter::handle` and + `drive_session`/`pump_session` — the per-stream dispatch and + bidirectional pump pattern the tunnel handler reuses. +- `crates/alknet-core/src/types.rs` — `Connection::from_stream`, + `SendStream::from_stream`, `RecvStream::from_stream`, `ProtocolHandler` + — the integration points the POC validates. +- `docs/research/alknet-docker/poc-summary.md` — the docker POC this doc + mirrors in structure and tone. \ No newline at end of file