Files
alknet/docs/architecture/crates/channels/channel-client.md
T
glm-5.2 a3cb44968e docs(adr): 093 — channels pure channel multiplexing (8-byte header, no stream_type)
Prune the channels spec to reflect the stream-unification resolution
(docs/research/stream-unification/findings.md): the channels wire format
goes from 9 bytes to 8 bytes, the channels layer no longer carries a
stream_type concept, into_sub_streams() is removed, and TTY always uses
its 5-byte format (carried transparently in the channels payload).

ADR-093 is the umbrella decision (the channels-layer consequence of
ADR-092's BiStream handler leaf): every channel is a BiStream, the
handler owns its sub-stream multiplexing, the channels layer routes by
channel_id only. Amends ADR-071 (8-byte header, no stream_type),
ADR-074 (into_sub_streams removed, accept_bi yields BiStream), reverses
ADR-077 (TTY always 5-byte), and the channels-facing clauses of
ADR-072/073/075/076/080/081. Adds ADR-092 forward-reference note
(into_sub_streams preservation subsequently reversed by ADR-093) and
the missing ADR-092 cross-reference on ADR-070.

Adds OQ-68 (add/strip API shape — built-in vs utility; the contract is
decided in ADR-093, the function surface is open; two-way door, low
priority, decision-ready when the channels crate's implementation
begins).

Rewrites the 7 channels spec docs (README, overview, channels-wire,
channels-connection, channels-adapter, channel-operations, channel-client)
to describe the post-amendment shape as current, with the 8-byte header,
the add/strip composition, single accept_bi accessor, BiStream per
channel, and TTY-always-5-byte.

Touch-up cross-references in hub README, client README, ADR-085, and
the OQ-45/47/65 question files (TTY-internal stream_type 3 →
STREAM_CTRL_IN; channels 9-byte → 8-byte).
2026-07-18 18:10:17 +00:00

221 lines
10 KiB
Markdown

---
status: draft
last_updated: 2026-07-18
---
# channel-client.md — ChannelClient
The client side of a channels connection. ADR-080 is the decision; this doc
specifies the API.
## What
`ChannelClient` is the symmetric counterpart to `ChannelsAdapter` (ADR-075).
The server side is a `ProtocolHandler` (`ChannelsAdapter::handle`); the
client side takes over an established transport `Connection`, runs the
demux/mux, and exposes `open_channel(alpn, params) -> Channel` to the
application.
This is the channels analogue of `CallClient` (server: `CallAdapter`;
client: `CallClient`) in the call protocol.
## API
```rust
pub struct ChannelClient {
manager: ChannelManager,
// The transport-side demux/mux, running in a background task.
...
}
impl ChannelClient {
/// Construct a `ChannelClient` over a pre-established transport
/// `Connection` on ALPN `alknet/channels`. This is the
/// transport-agnostic primary constructor: the caller (or a
/// transport-specific dial helper) produces the `Connection` —
/// via `Connection::from_bidi` (TCP+TLS, WebTransport, SSH
/// `direct-tcpip`), a quinn connection, or any other `AsyncRead +
/// AsyncWrite` source — and this method takes over: installs
/// channel 0 (`alknet/call`), spawns the demux/mux, and returns
/// the client. Mirrors the server side's transport-agnostic
/// `ChannelsAdapter::handle(Connection)` and
/// `CallClient::spawn_dispatch(Connection)`.
///
/// This is the one-way-door API surface (ADR-080). It must not be
/// coupled to a transport — the channels protocol is
/// transport-agnostic (ADR-071, as amended by ADR-093; ADR-065,
/// ADR-092), and the client side is half of that protocol.
pub async fn from_connection(connection: Connection)
-> Result<Self, ChannelError>;
/// QUIC convenience constructor. Dials a QUIC connection to `addr`
/// on ALPN `alknet/channels` (using `credentials` for the TLS
/// handshake — ADR-034 verifier selection), then calls
/// `from_connection`. This is the "I just want QUIC" one-liner;
/// it is additive over `from_connection` and is a two-way door —
/// `connect_tcp_tls`, `connect_webtransport`, etc. can be added
/// alongside it without touching the one-way-door surface.
///
/// **REMOVED per ADR-089 §5.** The dial is extracted into
/// `AlknetClient` (`alknet-client`); `connect_quic` is deleted,
/// not delegated, to avoid `alknet-channels-call` depending on
/// `alknet-client`. Callers compose `AlknetClient::dial_quic` +
/// `from_connection`. See "Relationship to `AlknetClient`" below.
/// The `CallCredentials` parameter is moot — `CallCredentials` is
/// removed per ADR-091 (amended 2026-07-17); the dial consumes
/// `ConnectionCredentials` from `alknet-core`.
pub async fn connect_quic(
addr: SocketAddr,
credentials: CallCredentials, // REMOVED — CallCredentials is removed
) -> Result<Self, ChannelError>;
/// Open a data channel with the given ALPN and params. Sends
/// `channel/open` on channel 0, waits for the response, and returns
/// the channel.
pub async fn open_channel(
&self,
alpn: &str,
params: Value,
direction: ChannelDirection,
) -> Result<Channel, ChannelError>;
/// Subscribe to the peer's resource updates. Returns a stream of
/// resource-set events (ADR-073 channel/resources/subscribe). Each
/// event carries the JSON `output.resources` array from ADR-073's
/// `channel/resources/subscribe` response shape.
pub async fn subscribe_resources(&self)
-> Result<BoxStream<ResourceEvent>, ChannelError>;
/// The call-protocol connection on channel 0, for invoking channel
/// lifecycle operations and any other call ops the peer exposes.
pub fn call(&self) -> &CallConnection;
}
pub enum ChannelDirection {
InitiatorToResponder,
ResponderToInitiator,
}
pub struct Channel {
pub channel_id: u32,
/// The channel's BiStream, accessible via the BidiStreamSource
/// (accept_bi — ADR-074 as amended by ADR-093).
pub source: ChannelBidiStreamSource,
}
/// One event from `channel/resources/subscribe`. Wraps the JSON `output`
/// object from ADR-073's subscribe response — the `resources` array
/// describing what ALPNs the peer exposes and with what `access` preview.
/// The channels crate maps the JSON to this typed struct; the fields mirror
/// ADR-073's response shape.
pub struct ResourceEvent {
pub resources: Vec<ResourceEntry>,
}
pub struct ResourceEntry {
pub alpn: String,
pub backends_or_targets: Vec<String>, // ALPN-specific enumeration
pub access: Value, // preview of AccessControl (advisory)
}
```
> **Amendment (ADR-093, 2026-07-18):** the `stream_types` field is
> **removed** from `open_channel`'s signature and from `Channel`. The
> channels layer has no `stream_type` concept (ADR-093) — the handler
> owns its sub-stream multiplexing on the `BiStream` it receives. The
> handler's sub-stream set is implicit in its ALPN's wire format.
## Transport-agnostic by construction
`ChannelClient` is the client side of the channels protocol. The channels
protocol is transport-agnostic (ADR-071 substrate modes, as amended by
ADR-093; `Connection::from_bidi`/`from_source` from ADR-065/070/092 take
any `AsyncRead + AsyncWrite`). The client side must not be welded to a
transport — that would repeat the server-side welding ADR-065 explicitly
unwound.
`from_connection(connection: Connection)` is the primary constructor and
the one-way-door API surface. It takes a pre-established `Connection` and
takes over channels establishment. The transport is the caller's concern:
`Connection::from_bidi(tls_stream, ...)` for TCP+TLS, a quinn `Connection`,
a WebTransport `BiStream`, an SSH `direct-tcpip` channel wrapped via
`from_bidi`, a WebSocket carrying `alknet/channels` (the browser path per
ADR-044) — all produce a `Connection` that `from_connection` accepts
unchanged. This mirrors the server side's `ChannelsAdapter::handle(Connection)`, which is substrate-agnostic by the same mechanism.
`connect_quic(addr, credentials)` was a **convenience** constructor —
dial QUIC, then `from_connection`. It is **removed** per ADR-089 §5:
keeping it as a thin wrapper over `AlknetClient::dial_quic` would make
`alknet-channels-call` depend on `alknet-client`, contradicting the dep
graph (the protocol crates are parallel to the dial, not downstream of
it). Callers compose `AlknetClient::dial_quic(...).await?` +
`ChannelClient::from_connection(conn).await?` — two lines, the dial
then the take-over.
The credential/verifier-selection rule (ADR-034) lives in the dial
(`AlknetClient`), not in `from_connection` — `from_connection` receives
an already-established, already-authenticated `Connection`, exactly as
`ChannelsAdapter::handle` does on the server side.
## Bidirectionality preserved
The channels protocol is bidirectional — either side can open a channel
(ADR-073 §direction semantics). `ChannelClient::open_channel` supports both
`ChannelDirection::InitiatorToResponder` and
`ChannelDirection::ResponderToInitiator`. The client is not "the client
side" in the request/response sense — it can also receive `channel/open`
requests from the peer (the peer initiates, the client's `ChannelManager`
responds). This mirrors the call protocol's operation overlay (each side
populates what operations they expose).
`ChannelClient` is one endpoint of a bidirectional channels connection. The
name follows the `CallClient` convention (the side that dialed), not a
request/response role.
## Relationship to `AlknetClient` (ADR-089 — resolved)
`ChannelClient`'s *API* is transport-agnostic — `from_connection` takes a
pre-established `Connection`. The shared *dial+TLS* seam
(`AlknetClient`, OQ-55) is now extracted: [`alknet-client`](../client/README.md)
provides `AlknetClient` with three dial methods (`dial_quic` /
`dial_tcp_tls` / `dial_iroh`), each producing a `Connection` that
`from_connection` consumes. The dial is transport-specific (QUIC,
TCP+TLS, iroh); the take-over (`from_connection`) is
transport-agnostic. The two concerns are separated.
`connect_quic` is removed (see above) — `AlknetClient::dial_quic` is the
dial that feeds `from_connection`. A caller that needs transport
selection (QUIC with TCP+TLS fallback) uses `AlknetClient` directly;
the fallback policy is a caller concern. See
[ADR-089](../../decisions/089-alknetclient-native-dial-seam.md) for the
full decision and [OQ-55](../../questions/055-alknetclient-establishment-extraction.md)
(resolved).
## Design Decisions
All design decisions are documented as ADRs in [decisions/](../../decisions/).
| ADR | Decision | Summary |
|-----|----------|---------|
| [080](../../decisions/080-channelclient.md) | ChannelClient | Client side; transport-agnostic `from_connection` primary; `connect_quic` convenience **removed** per ADR-089 §5 (dial extracted to `AlknetClient`); `AlknetClient` dial-seam extracted (ADR-089, resolves OQ-55) |
| [093](../../decisions/093-channels-pure-channel-multiplexing.md) | channels Pure Channel Multiplexing | `stream_types` removed from `open_channel` and `Channel`; handler owns sub-stream multiplexing |
## Open Questions
- **OQ-55** (resolved by ADR-089): `AlknetClient` core **dial+TLS seam**
— extracted as `alknet-client` with three dial methods.
`ChannelClient`'s API is transport-agnostic (`from_connection`); the
dial is the shared seam, now extracted. See
[ADR-089](../../decisions/089-alknetclient-native-dial-seam.md).
## References
- ADR-080: ChannelClient (the decision)
- ADR-093: channels pure channel multiplexing (`stream_types` removed)
- ADR-073: channel lifecycle operations (`open_channel` sends `channel/open`)
- ADR-074: ChannelBidiStreamSource (what `Channel.source` wraps, as
amended by ADR-093 — `accept_bi` yields a `BiStream`)
- ADR-075: ChannelManager (the shared state `ChannelClient` holds)
- OQ-55: AlknetClient / client establishment extraction
- `docs/architecture/crates/call/client-and-adapters.md` — `CallClient` (the
shape `ChannelClient` mirrors)