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).
10 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 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
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_typesfield is removed fromopen_channel's signature and fromChannel. The channels layer has nostream_typeconcept (ADR-093) — the handler owns its sub-stream multiplexing on theBiStreamit 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
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 for the
full decision and OQ-55
(resolved).
Design Decisions
All design decisions are documented as ADRs in decisions/.
| ADR | Decision | Summary |
|---|---|---|
| 080 | 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 | 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):
AlknetClientcore dial+TLS seam — extracted asalknet-clientwith three dial methods.ChannelClient's API is transport-agnostic (from_connection); the dial is the shared seam, now extracted. See ADR-089.
References
- ADR-080: ChannelClient (the decision)
- ADR-093: channels pure channel multiplexing (
stream_typesremoved) - ADR-073: channel lifecycle operations (
open_channelsendschannel/open) - ADR-074: ChannelBidiStreamSource (what
Channel.sourcewraps, as amended by ADR-093 —accept_biyields aBiStream) - ADR-075: ChannelManager (the shared state
ChannelClientholds) - OQ-55: AlknetClient / client establishment extraction
docs/architecture/crates/call/client-and-adapters.md—CallClient(the shapeChannelClientmirrors)