The hub README predates channels (2026-07-09) and was built on the call-protocol-directly-over-QUIC model: 'One QUIC connection per peer carrying the call protocol,' CallClient::connect as the dial path, QUIC as the only transport. The channels crate replaced that substrate (ADR-071/079/080), and the hub's primary use case (worker provisioning on docker/vast.ai/runpod) requires TCP+TLS for registration and QUIC or TCP+TLS for the ongoing session — coexisting on one hub, not as alternatives. Rewrite: - Multi-transport stated as decided: 'A hub MUST support TCP+TLS and QUIC endpoints simultaneously.' TCP+TLS accept loop (ADR-010 Am. 1) lives in the hub, shares the HandlerRegistry with the quinn endpoint. - Channels substrate: the hub uses ChannelsAdapter/ChannelClient (from_connection primary, connect_quic convenience — ADR-080), not CallClient/CallAdapter directly. One channels connection per peer; CallAdapter runs on channel 0 (ADR-072). - Transport-agnostic dial/accept: dial_worker_connection takes a Connection (one-way door); connect_quic_worker is a two-way-door convenience. supervise_worker takes a dial closure, not a SocketAddr. - Identity over transports: fingerprint path (QUIC+raw-key, X.509 client cert) via resolve_from_fingerprint; bearer-token path (TCP+TLS no client cert, WebTransport, WebSocket) via resolve_from_token on the call first frame. Both resolve to the same PeerEntry (ADR-030/034). - Worker registration flow (6 steps): provision → token → download → key gen → HTTP POST over TCP+TLS → channels connect. Step 4 is HTTP on HttpAdapter; step 6 is channels over QUIC or TCP+TLS. The hub creates a mixed-fingerprint PeerEntry (ADR-034 §3) at registration. - OQ-58: worker registration flow — enrollment-token model, endpoint shape, register_worker API. Open (decision-ready, not blocked), one-way door, high priority. - ADR-081: fix stale references to channels-hub/channels-worker sub-crates (the ADR's Decision says they don't exist; the References section said they did). Review (architecture-reviewer): 3 critical (ADR-081 stale refs; bearer-token extraction flow; assembly example QUIC-only + unspecced into_connection), 7 warnings (registration 'or'→'both', channel/control translation, OQ-52 interim, HubError variant naming, RegistrationError definition, X.509-client-cert prose, adapter ownership phrasing), 5 suggestions. All criticals and warnings #4/#7/#8 addressed; #5/#6/#9/#10 addressed; #11-15 noted as optional.
11 KiB
ADR-081: alknet-channels Sub-Crate Decomposition
Status
Accepted
Context
The initial channels spec (ADR-071-080) framed alknet-channels as a
single crate depending on both alknet-core and alknet-call. The
dependency on alknet-call arises because channel lifecycle operations
(channel/open, channel/close, channel/control,
channel/resources/subscribe) register on the call protocol's
OperationRegistry, and channel 0 is pre-negotiated as alknet/call
(ADR-072).
This creates two issues:
-
The dependency graph conflates the pure multiplexer with the call- protocol coupling. The wire format, demux/mux, and
ChannelBidiStreamSourceare ALPN-blind and call-protocol-blind — they depend onalknet-coreonly. The channel 0 pre-negotiation and lifecycle op registrations are the call-protocol coupling. Baking both into one crate means any consumer that wants the multiplexer also pulls in the call-protocol coupling, even if they don't need channel 0 to bealknet/call. -
The "no special-casing for downstream crates" principle is violated. The user's constraint: "we don't want to be doing anything special for downstream crates unless it's needed/useful to the point of they all kind of benefit." The call-protocol coupling is a channels-crate concern (channels needs call for orchestration), not a downstream-crate concern. Separating them makes the dependency graph honest: the pure multiplexer has no opinion about channel 0; the call-protocol coupling is isolated where it belongs.
The substrate simplification (ADR-071 revised) strengthened this: the wire
format is the same across all substrates, and the ChannelsAdapter is
substrate-agnostic. The pure multiplexer (channels-core) is cleanly
separable from the call-protocol orchestration (channels-call).
Decision
Two crates, not four
alknet-channels-core — the pure multiplexer (wire format, demux/mux,
│ ChannelBidiStreamSource, ChannelManager). Depends
│ on alknet-core only. ALPN-blind, call-protocol-blind,
│ transport-blind.
└── alknet-channels-call — channel 0 pre-negotiation + lifecycle op
registrations on the call protocol's
OperationRegistry. Depends on channels-core +
alknet-call.
There are no channels-hub or channels-worker sub-crates. The hub and
worker are consumers of channels, not sub-crates of it. The existing
alknet-hub crate (docs/architecture/crates/hub/README.md) IS the hub —
it depends on channels-call and uses the channels protocol as its
substrate. A worker is just a worker — it depends on channels-call and
uses ChannelClient (ADR-080) to dial. The hub relay logic (ADR-079) lives
in alknet-hub, not in a channels sub-crate.
alknet-channels-core
The pure multiplexer. Contains:
- The 9-byte chunk wire format (
parse_header/write_header— ADR-071) - The demux/mux (
Demux,MuxHandle/MuxRunner— ADR-075) ChannelBidiStreamSource(implementsBidiStreamSource— ADR-074)ChannelSubStreams/SubStreamHandle(the typed destructure accessor)ChannelManager(the shared state — channel_id → ChannelState, but without thecall_ops: Arc<OperationRegistry>field; the manager is ALPN-blind and call-protocol-blind)ChannelsAdapter(theProtocolHandleronalknet/channels— the read/demux loop, substrate-agnostic per ADR-071 revised)
Depends on alknet-core only. No alknet-call dependency. No opinion about
what channel 0 carries — that's the consumer's concern.
The ChannelsAdapter::handle in channels-core does NOT preinstall channel
0 as alknet/call. It runs the demux loop and routes chunks by
channel_id. Channel 0 is just another channel; what ALPN it carries is
determined by the consumer (the channels-call crate pre-negotiates it as
alknet/call; a hypothetical other consumer could pre-negotiate it
differently).
alknet-channels-call
The call-protocol coupling. Contains:
- Channel 0 pre-negotiation as
alknet/call(ADR-072) — thepreinstall_channel_0logic that constructs channel 0's reassembly buffers with stream_types [0, 1] and hands theConnectionto theCallAdapter. - The four lifecycle operations (ADR-073):
channel/open,channel/close,channel/control,channel/resources/subscribe— registered on the call protocol'sOperationRegistryat assembly time. ChannelOperations(the registration helper that closes over aChannelManagerclone).ChannelClient(ADR-080) — the client-side type that dials a transport, establishes the channels connection, and exposesopen_channel(alpn, params) -> Channel. This is the worker/client entry point; it lives here because it needs channel 0 pre-negotiation (which is inchannels-call).
Depends on channels-core + alknet-call. This is where the
call-protocol coupling lives, isolated from the pure multiplexer.
Hub and worker are consumers, not sub-crates
The hub and worker are architectural roles, not channels sub-crates:
-
The hub is the existing
alknet-hubcrate. It depends onchannels-calland uses the channels protocol as its substrate. The hub relay logic (ADR-079 — translatechannel/openon channel 0, byte-forward data channels withchannel_idrewrite) lives inalknet-hub, alongside its existing peer lifecycle, aggregated env, and service discovery responsibilities. There is nochannels-hubsub-crate;alknet-hubIS the channels hub. -
A worker is any crate that uses
ChannelClient(ADR-080, inchannels-call) to dial a hub. There is nochannels-workersub-crate; a worker depends onchannels-calland usesChannelClientdirectly. The worker may be a CLI binary, a docker-side connector, an SSH-side connector, or any other role that dials into a hub's channels connection.
This means the channels crate provides the substrate (channels-core +
channels-call); the hub and worker crates are consumers that build on it.
The dependency direction is: alknet-hub → channels-call →
channels-core → alknet-core; a worker → channels-call →
channels-core → alknet-core. The channels crate has no dependency on
alknet-hub or any worker crate.
What moves where
| Component | Original (ADR-071-080) | Now |
|---|---|---|
| 9-byte wire format | alknet-channels |
channels-core |
| Demux/Mux | alknet-channels |
channels-core |
ChannelBidiStreamSource |
alknet-channels |
channels-core |
ChannelSubStreams |
alknet-channels |
channels-core |
ChannelManager (without call_ops) |
alknet-channels |
channels-core |
ChannelsAdapter (demux loop only) |
alknet-channels |
channels-core |
| Channel 0 pre-negotiation | alknet-channels (ADR-072) |
channels-call |
channel/open/close/control/resources/subscribe ops |
alknet-channels (ADR-073) |
channels-call |
ChannelOperations registration helper |
alknet-channels |
channels-call |
ChannelClient (ADR-080) |
alknet-channels |
channels-call |
| Hub relay (ADR-079) | alknet-channels (spec) |
alknet-hub (the existing hub crate, consuming channels-call) |
Relationship to alknet-hub
The existing alknet-hub crate (docs/architecture/crates/hub/README.md)
is the hub pattern: peer lifecycle, aggregated env, service discovery. With
channels as the substrate, alknet-hub gains a dependency on
channels-call and incorporates the relay logic (ADR-079). The hub spec
(crates/hub/README.md) will be updated to reflect that the hub uses
channels as its transport substrate — one channels connection per leg
(browser↔hub, hub↔spoke), with the relay translating channel/open and
byte-forwarding data channels. The hub's existing responsibilities (peer
lifecycle, aggregated env, service discovery, worker supervision) are
unchanged; channels is the substrate they run on.
This makes "channels hub" and "hub" the same thing — the hub IS built on channels. There is no separate channels-hub concept.
Consequences
Positive:
- The dependency graph is honest:
channels-coreis the pure multiplexer with no call dependency; the call-protocol coupling is isolated inchannels-call. A consumer that wants the multiplexer without the call-protocol orchestration can depend onchannels-coreonly. - The "no special-casing for downstream crates" principle is preserved:
channels-coredoesn't know aboutalknet-call,alknet-tty, or any handler crate. The call-protocol coupling is a channels-crate concern, not a downstream-crate concern. - Hub and worker are consumers, not sub-crates. The existing
alknet-hubcrate IS the channels hub — it depends onchannels-calland uses channels as its substrate. A worker depends onchannels-calland usesChannelClient. The channels crate has no dependency onalknet-hubor any worker crate. This is the cleanest dependency direction: channels provides the substrate; hub and worker consume it. - The WASM and cross-platform story gets easier:
channels-coreis WASM-compatible by construction (pure byte manipulation, no platform deps);channels-callinherits the call protocol's WASM constraints; the hub and worker crates are platform-specific as needed.
Negative:
- Two channels crates instead of one. The assembly layer must depend on
channels-core+channels-callinstead of onealknet-channels. This is the cost of the clean separation; the assembly layer already wires multiple crates, so this is consistent with the existing pattern. - The
ChannelsAdapterinchannels-coredoesn't preinstall channel 0 — the consumer does. This meanschannels-core'sChannelsAdapter::handleexposes a hook (callback or trait method) for the consumer to install channel 0.channels-callprovides thepreinstall_channel_0implementation; a different consumer could provide a different one. This is a slightly more complex adapter shape than "channel 0 is always call," but it's the cost of the clean separation.
Door type
One-way (crate structure). The two-crate split (channels-core /
channels-call) is one-way — once consumers depend on channels-core
without channels-call, re-merging them is a breaking change. The hub and
worker being consumers (not sub-crates) is also one-way — it establishes
the dependency direction (hub/worker → channels, not channels → hub/worker).
References
- ADR-003: crate decomposition (no-handler-depends-on-another-handler — preserved; the channels sub-crates depend on core/call, not on handlers)
- ADR-071: channels wire format (revised — substrate simplification; the
wire format is in
channels-core) - ADR-072: channel 0 pre-negotiated (moves to
channels-call) - ADR-073: channel lifecycle operations (move to
channels-call) - ADR-074: ChannelBidiStreamSource (in
channels-core) - ADR-075: ChannelsAdapter and ChannelManager (split: core demux in
channels-core, call coupling inchannels-call) - ADR-079: hub relay (in
alknet-hub— the hub crate consumeschannels-call; there is nochannels-hubsub-crate, per this ADR's Decision §"No hub/worker sub-crates") - ADR-080: ChannelClient (in
channels-call; there is nochannels-workersub-crate — a worker is any crate that usesChannelClientto dial) docs/architecture/crates/hub/README.md— the existing hub crate (the relay's consumer)