Files
alkcall/docs/architecture/channels-connection.md
T
glm-5.3-flash 5f019c02f1 docs: re-key architecture docs off the alknet mono-repo — speak as alkcall
The ported docs still framed alknet siblings (alknet-core/-call/-channels/
-http/-tls/-client) as live load-bearing context of this crate. With the
extraction complete and alknet headed for glue-level replacement, the docs
now speak as alkcall and cite the real sibling crates (alkhttp, alktls,
alktty, alktunnels, alktrader).

- ADR-054 (new): re-own the verifier-selection rule in-crate — three
  remote roles (public X.509 endpoint / transport relay / hub),
  ServerCertVerifier by PeerEntry presence, RemoteIdentity None
  load-bearing. Provenance: alknet ADR-034 (never ported), demoted to
  footnote. Resolves OQ-21's cross-repo authority gap.
- OQ-43 (new, deferred(scope)): composition-layer ownership and shape
  after the alknet replacement — the named receiver for the
  downstream-side deferrals (OQ-33 endpoint layer, OQ-41 QUIC-native
  substrate, OQ-42 hub broker, OQ-19 persistence adapters, ADR-045 dial
  seam). README layering diagram redrawn with the composition layer as
  TBD-ownership instead of "alknet / alknode" in concrete.
- Spec docs reframed: titles, self-references, dial/TLS/from_jsonschema
  homes, malformed legacy cite rows removed; dead relative research
  paths qualified as source-repo paths.
- ADR amendments (append-only): 027 (alknet-http -> alkhttp), 031 (post-
  extraction state), 044 (split landed as modules in alkcall), 045
  (name normalization; citation-space note).
- Citation hygiene: unprefixed/colliding source-repo cites prefixed
  ("alknet ADR-NNN (provenance)") across 009/011/022/034/035/038/040/
  043/044/045; stale OQ cites corrected (session registries OQ-04, batch
  semantics OQ-03, verifier rule ADR-054); stale anchors fixed.
- README provenance note consolidated; citation conventions documented.

Verification: markdown link check (all local links resolve, all in-crate
ADR cites point at existing decisions/ files); table integrity check
(uniform row widths across open-questions.md and edited tables); residual
alknet-mention sweep (only provenance/historical/intentional framing
remains); architecture review subagent pass with zero remaining criticals.
Docs-only change — no code paths touched.
2026-10-13 10:53:47 +00:00

7.7 KiB

status, last_updated
status last_updated
draft 2026-10-13 (alkcall reframing)

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 alkcall's vendored-core 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

// In alkcall (channels subsystem):

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.

// 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-050)
    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(())
}
// 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 alk/channels can open a sub-channels connection on a data channel — alk/channels inside alk/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 BiStreams; 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/.

ADR Decision Summary
038 ChannelConnection Per-channel BidiStreamSource; yield-once accept_bi is the only accessor
035 channels Pure Channel Multiplexing The umbrella decision: 8-byte header, no stream_type, BiStream-only
008 BidiStreamSource Trait The extension point ChannelBidiStreamSource implements
009 BiStream as the Handler Leaf accept_bi returns BiStream (the transport-leaf decision this doc builds on)
007 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)
  • Provenance (source-repo, not ported): alknet ADR-077 — TTY inside channels (TTY always uses its 5-byte format, carried transparently in the channels payload)
  • Source-repo POC research (alknet provenance): /workspace/@alkdev/alknet/docs/research/alknet-channels/poc-summary.md §POC Target 2 (the yield-once Connection::from_stream validation); /workspace/@alkdev/alknet/docs/research/stream-unification/findings.md — the research that surfaced the single-accessor resolution