Files
alkcall/docs/architecture/channels-connection.md
glm-5.2 cc470a363a docs: port architecture specs + 45 ADRs from alknet, renumbered
Port the call + channels architecture documentation from the alknet
mono-repo into docs/architecture/, renumbered as alkcall ADR-001..045.

Renumbering map (alknet -> alkcall):
  Core:        001,002,004,006,007,011,065,070,092,014,050,091 -> 001-012
  Call:        005,064,012,023,015,022,024,016,049,017,028,029,030,032,066,069,067,068 -> 013-030
  Shared:      003,009,013 -> 031-033
  Channels:    071,093,072,073,074,075,076,094,079,080,081,089 -> 034-045

3 superseded/reversed ADRs kept for historical trail:
  - ADR-013 (irpc foundation, superseded by ADR-014)
  - ADR-023 (peer-scoped filtering, superseded by ADR-024)
  - ADR-077 (TTY inside channels, reversed by ADR-035 — not ported, TTY-only)

Ported docs (11 spec files + README + open-questions):
  - call-README.md, call-protocol.md, operation-registry.md, client-and-adapters.md
  - channels-README.md, channels-overview.md, channels-wire.md, channels-connection.md, channels-adapter.md, channel-operations.md, channel-client.md
  - README.md (index with doc table, ADR table grouped by category, key principles)
  - open-questions.md (lean — 30 OQs, renumbered OQ-01..030; includes new OQ-22 for the pub/sub gap)

Cross-reference rewriting:
  - All ADR-NNN references rewritten single-pass (no chaining bug)
  - Markdown link paths fixed
  - Title lines aligned with filenames
  - Non-ported ADR refs (052, 082, 086, etc.) left as-is with README note

The open-questions.md includes OQ-22 (new): the call protocol pub/sub
gap — subscribe exists but pub does not, needed for channels
channel/resources/subscribe fan-out. This is the next ADR to write
(alkcall ADR-046).
2026-08-12 07:06:57 +00:00

179 lines
7.4 KiB
Markdown

---
status: draft
last_updated: 2026-07-18
---
# channels-connection.md — ChannelBidiStreamSource and `BiStream` Access
How a reassembled channel is presented to its handler as a `Connection`.
ADR-038 (amended by ADR-035) is the decision; this doc specifies the API
shape — one accessor, one `BiStream` per channel.
## What
Each channel is reassembled into a `BiStream` — a single duplex
(`AsyncRead + AsyncWrite`) byte stream. The channels layer strips its
8-byte header (`channel_id` + `length`) on read, hands the payload to the
reassembled `BiStream`, and the handler parses its own framing from the
payload. The handler sub-multiplexes its `BiStream` however it wants —
TTY sub-demuxes `stream_type` from its `BiStream` via its 5-byte format,
tunnel uses the `BiStream` as raw bytes, call length-prefixes JSON, SSH
runs its own channel protocol.
The `BiStream` is wrapped in a `ChannelBidiStreamSource` that implements
`alknet-core`'s `BidiStreamSource` trait (ADR-008), and a `Connection` is
constructed from it via `Connection::from_source(source, alpn)`. The
handler receives a `Connection`, calls `accept_bi()` once (yield-once per
channel), gets a `BiStream`, and drives its session — identical to how it
works on a top-level QUIC connection.
## `ChannelBidiStreamSource`
```rust
// In alknet-channels:
pub struct ChannelBidiStreamSource {
// The reassembly buffer for this channel's payload bytes (one per
// channel_id, not per (channel_id, stream_type) — the channels layer
// has no stream_type concept), plus the mux handle for writing back
// onto the transport. Constructed by ChannelManager::build_channel_connection
// (ADR-039).
...
}
#[async_trait]
impl BidiStreamSource for ChannelBidiStreamSource {
async fn accept_bi(&self)
-> Result<BiStream, StreamError>
{
// Yields the channel's BiStream on first call,
// ConnectionClosed on subsequent calls. Yield-once per channel,
// matching the POC's validated shape.
}
async fn open_bi(&self)
-> Result<BiStream, StreamError>
{
// StreamClosed — a single channel cannot open new application
// streams (same as ADR-007's Stream backend). The handler owns
// its sub-stream multiplexing on the BiStream it received.
}
fn remote_addr(&self) -> Option<SocketAddr> { ... }
fn close(&self, _code: u32, _reason: &str) { ... }
}
```
One `ChannelBidiStreamSource` instance represents **one channel** (not the
whole channels connection). The `ChannelManager` (ADR-039) constructs one
per channel at `channel/open` time and wraps it in a `Connection` via
`from_source`.
## The single path: `accept_bi()`
Every handler — TTY, tunnel, SSH, call — receives a `Connection`, calls
`accept_bi()` once, gets a `BiStream`, and sub-multiplexes it however it
wants. There is one accessor.
```rust
// Tunnel handler — ~15 lines, zero channels-layer awareness
async fn handle(&self, connection: Connection, _auth: &AuthContext)
-> Result<(), HandlerError>
{
let mut bidi = connection.accept_bi().await?;
let mut tcp = TcpStream::connect(target).await?;
let (mut tcp_read, mut tcp_write) = tcp.into_split();
let (mut recv, mut send) = tokio::io::split(&mut bidi);
// Two-pump with shutdown-on-completion (ADR-078)
let c2t = async {
tokio::io::copy(&mut recv, &mut tcp_write).await?;
tcp_write.shutdown().await.ok();
Ok::<_, std::io::Error>(())
};
let t2c = async {
tokio::io::copy(&mut tcp_read, &mut send).await?;
send.shutdown().await.ok(); // emits zero-length sentinel (REQ-CH-01)
Ok::<_, std::io::Error>(())
};
tokio::try_join!(c2t, t2c)?;
Ok(())
}
```
```rust
// TTY handler (inside-channels mode) — the SAME code as direct
// mode, just a different BiStream source.
async fn handle(&self, connection: Connection, _auth: &AuthContext)
-> Result<(), HandlerError>
{
let mut bidi = connection.accept_bi().await?;
// drive_session reads the 5-byte TTY chunks off `bidi` — the same
// code as direct mode. The channels layer stripped its 8-byte
// header; TTY's 5-byte format is the payload.
drive_session(bidi, backends, ownership, identity).await
}
```
The handler calls `accept_bi()` once, gets a `BiStream`, and pumps. It
does not know it's inside a channels connection — the `Connection` looks
like any other. This is the path the POC's `EchoHandler` and
`TunnelHandler` validated.
`accept_bi()` is yield-once: the first call returns the `BiStream`;
subsequent calls return `ConnectionClosed`. This matches the POC's
validated shape and the `StreamBidiStreamSource` yield-once contract
(ADR-008, ADR-009).
## Recursive composition
A `ChannelBidiStreamSource` is a `BidiStreamSource`, and
`Connection::from_source` wraps it. A handler that is itself
`alknet/channels` can open a sub-channels connection on a data channel —
`alknet/channels` inside `alknet/channels`. The outer layer strips its
8-byte header; the inner layer parses its own 8-byte header from the
payload. Each level is the same shape: `BiStream → accept_bi → N
BiStreams`. The recursion is unbounded and uniform at every level.
This is a property, not a feature. The primary use case is one level of
multiplexing. But the add/strip composition makes it cleaner than
ADR-034's group framing did — the recursion is the same operation
(strip an 8-byte header) at every level, not a different framing per
level.
## What does NOT change
- **`ProtocolHandler` trait** (ADR-002) — handlers still receive a
`Connection` and call `accept_bi()`. The `ChannelBidiStreamSource` is
internal to the channels crate; handlers see a `Connection`.
- **`BiStream`** (ADR-009) — the leaf type `accept_bi` returns. The
channels layer yields `BiStream`s; handlers parse them per their ALPN.
- **`HandlerRegistry`** — unchanged. The channels layer looks up ALPNs in
the same registry as top-level connections.
## Design Decisions
All design decisions are documented as ADRs in [decisions/](decisions/).
| ADR | Decision | Summary |
|-----|----------|---------|
| [074](decisions/074-channelconnection-bidistreamsource.md) | ChannelConnection | Per-channel `BidiStreamSource`; yield-once `accept_bi` is the only accessor |
| [093](decisions/093-channels-pure-channel-multiplexing.md) | channels Pure Channel Multiplexing | The umbrella decision: 8-byte header, no `stream_type`, `BiStream`-only |
| [070](decisions/070-bidistreamsource-trait.md) | BidiStreamSource Trait | The extension point `ChannelBidiStreamSource` implements |
| [092](decisions/092-bistream-as-the-handler-leaf.md) | `BiStream` as the Handler Leaf | `accept_bi` returns `BiStream` (the transport-leaf decision this doc builds on) |
| [065](decisions/065-connection-from-stream-generic-single-stream.md) | `Connection::from_stream` | The yield-once path generalized for channels |
## References
- ADR-038: ChannelConnection (the decision)
- ADR-035: channels pure channel multiplexing (the umbrella decision)
- ADR-008: BidiStreamSource trait
- ADR-009: `BiStream` as the handler leaf
- ADR-007: `Connection::from_stream` (the yield-once path generalized)
- ADR-077: TTY inside channels (TTY always uses its 5-byte format,
carried transparently in the channels payload)
- `docs/research/alknet-channels/poc-summary.md` §POC Target 2 (the
yield-once `Connection::from_stream` validation)
- `docs/research/stream-unification/findings.md` — the research that
surfaced the single-accessor resolution