The spec docs had not caught up with the stub-sweep fix commits (4eae18f..3317ea0), and the ported docs still carried alknet numbering that now collides with alkcall's own ADR range. Spec sync with the landed code: - call-protocol.md: register_imported/register_imported_all return Result and refuse marked specs; marked-spec registration refusal contract spelled out (ADR-047 am. 4 §4.8, ADR-022 am. 2026-10-13) - client-and-adapters.md: from_call flow gains the Pub sink-forwarding branch and the marked-op refusal (resolves OQ-37 — no from_call relay wrapper; sanctioned compositions are ChannelRelay::register_relay_openable / ChannelClient) - ADR-051: "channel/control is not implemented" stale sentence replaced with the ADR-052 per-leg control contract - OQ-37 resolved; OQ-22 split (primitive resolved; broker placement out of scope) with new OQ-42 for the downstream hub broker (deferred(scope), concretely blocked); OQ-24 resolved by the shipped add/strip surface; OQ-41 → deferred(scope) with blocking condition - README: OQ bullets renumbered to tracker IDs; alknet-collision note rewritten (range 001..053, alknet prefix convention); new principle 11 (implement-or-stop-advertising); AGENTS.md ADR range 001..053 Port-debt repair: - 149 broken decision links across 17 docs — ported with alknet numbers/paths, most now remapped to alkcall files by slug; the rest link the alknet repo per the README convention - stale OQ numbering rewritten to tracker IDs in call-protocol.md, call-README.md, operation-registry.md, client-and-adapters.md, ADR-025, channels-overview/README/wire, channel-client - unambiguous "alknet ADR-NNN" labels where an alknet number collides with an alkcall ADR (052 TTY vs channel/control, 033 storage vs rust-canonical, 025/026/027+020 vault vs PeerEntry/auth ADRs) Verified: cargo test (697 + guard), fmt, clippy --all-targets -D warnings; full markdown link scan clean.
12 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-07-18 |
alknet-channels
A multiplexing proxy: a ProtocolHandler on alk/channels that
decomposes a single bidirectional transport stream into N logical channels,
each carrying a different ALPN. Channel 0 is pre-negotiated as alk/call
(ADR-036); every other channel is opened dynamically via call operations on
channel 0 and routed through the same HandlerRegistry as top-level
connections. The channels layer is a re-framing proxy — it converts between
"one transport stream carrying N channels" (the wire) and "N independent
BiStream handles" (what handlers see) — and it does no protocol work
itself. The channels layer has no stream_type concept (ADR-035); the
handler owns its sub-stream multiplexing on the BiStream it receives.
Documents
| Document | Status | Description |
|---|---|---|
| overview.md | draft | Crate purpose, the multiplexing collapse, dependencies, ALPN, transport agnosticism, WASM, relationship to existing crates |
| channels-wire.md | draft | The 8-byte chunk format ([channel_id:u32 be][length:u32 be][payload]), the add/strip composition, sentinels, framing disambiguation, wire-level invariants (REQ-CH-01..05) |
| channels-connection.md | draft | ChannelBidiStreamSource (implements BidiStreamSource — ADR-008/074, as amended by ADR-035), accept_bi yields one BiStream per channel, recursive composition |
| channels-adapter.md | draft | ChannelsAdapter (ProtocolHandler on alk/channels), ChannelManager, demux/mux contracts (REQ-CH-01..04), the two-pump pattern (ADR-078) |
| channel-operations.md | draft | channel/open, channel/close, channel/control, channel/resources/subscribe — call-protocol operations on channel 0, ACL flow, direction semantics, the hub relay contract (ADR-042) |
| channel-client.md | draft | ChannelClient — the client side of a channels connection; transport-agnostic from_connection primary; dial lives in AlknetClient (ADR-045); bidirectionality preserved |
Applicable ADRs
| ADR | Title | Relevance |
|---|---|---|
| 034 | channels Wire Format — 8-Byte Chunk Header | The chunk format; channels layer has no stream_type concept (amended by ADR-035); substrate-agnostic; one-way door |
| 035 | channels Pure Channel Multiplexing | The umbrella decision: 8-byte header, no stream_type, into_sub_streams removed, BiStream-only, TTY always 5-byte |
| 036 | Channel 0 Is Pre-Negotiated alk/call |
Channel 0 = call protocol; no special control plane |
| 037 | Channel Lifecycle Operations on the Call Protocol | channel/open/close/control/resources/subscribe; direction semantics; subscribe not poll |
| 038 | ChannelConnection — BidiStreamSource over Chunk Reassembly | Per-channel BidiStreamSource impl; accept_bi yields BiStream (amended by ADR-035 — into_sub_streams removed) |
| 039 | ChannelsAdapter and ChannelManager | Substrate-agnostic demux loop; REQ-CH-01..04 contracts |
| 040 | Backpressure, Channel Limits, and ID Reuse | Bounded-buffer (1 MiB default), 256-channel per-connection memory bound, monotonic IDs with wrap (DoS defense reframed by ADR-041) |
| 041 | Per-Identity Channel Cap | 256 per PeerId, enforced via ChannelLifecyclePolicy in channels-call; per-connection max_channels reframed as a memory bound; symmetric (both sides enforce); spoke caps hub (direct caller), not browser (forwarded_for is metadata) |
| [077077 | TTY Inside Channels — Sub-Streams, Not Wire Format | TTY's two modes (direct vs channels); TTY always uses its 5-byte format, carried transparently in the channels payload |
| 050 | Two-Pump Shutdown-on-Completion Pattern | The two-pump deadlock contract; handler-level, not channels-layer |
| 042 | Hub Relay — Translate, Not Transparently Forward | The hub translates channel 0, byte-forwards data channels with ID rewrite |
| 043 | ChannelClient — the Client Side of a Channels Connection | ChannelClient, transport-agnostic from_connection primary; dial lives in AlknetClient (ADR-045, resolves OQ-55) |
| 044 | channels Sub-Crate Decomposition | channels-core (pure multiplexer) / channels-call (call coupling + ChannelClient); hub and worker are consumers, not sub-crates |
| 008 | BidiStreamSource Trait | The Connection extension point ChannelBidiStreamSource implements |
| 009 | BiStream as the Handler Leaf |
accept_bi returns BiStream; the transport-leaf decision ADR-035 builds on |
| 007 | Connection::from_stream |
The transport-agnostic Connection the channels layer rides on |
| [052052 | alknet-tty Wire Format | The 5-byte format carried transparently in the channels payload (control bidirectional via STREAM_CTRL_IN/OUT — Phase 7 amendment) |
| 021 | StreamingHandler for Subscriptions | The machinery channel/resources/subscribe uses |
| 026 | Forwarded-For Identity | The auth chain for hub-relayed channel opens |
| 031 | Crate Decomposition | alknet-channels depends on alknet-core only; no handler-depends-on-handler |
Relevant Open Questions
| OQ | Title | Status | Relevance |
|---|---|---|---|
| alknet OQ-55 | AlknetClient / Client Establishment Extraction | resolved (ADR-045) | ChannelClient's API is decided (ADR-043): transport-agnostic from_connection primary; dial lives in AlknetClient (alknet-client, ADR-045) |
| OQ-23 (tracker; alknet OQ-56) | Full channel-level flow-control windowing | deferred(scope) | Bounded-buffer is decided (ADR-040); full windowing is an extension blocked on "a real deployment observes HOL blocking on a saturated channel where bounded buffer is insufficient" |
| alknet OQ-57 | Two-pump helper extraction to alknet-core | deferred(scope) (alknet-scope) | The shutdown-on-completion contract is decided and the helper landed in-tree (channels::pump_bidi, ADR-050); any alknet-core extraction is alknet-scope |
| OQ-24 (tracker; alknet OQ-68) | Add/strip API shape (built-in vs utility) | resolved | Whether the 8-byte header add/strip is built into the channels read/write path or exposed as a standalone utility. The contract is decided (ADR-035); the function surface is not |
Key Design Principles
-
Streams are streams. A TTY session, an SSH channel, a forwarded TCP connection, a QUIC bidi stream — they're all
BiStream(a concreteAsyncRead + AsyncWritenewtype, per ADR-009). The differences are only in how they're opened (negotiation viachannel/openon channel 0) and what multiplexing layer carries them (the 8-byte chunk format). Once normalized, every channel is an ALPN routed through the sameHandlerRegistry. See overview.md and ADR-034 (as amended by ADR-035). -
Channel 0 is
alk/callpre-negotiated, not a special control plane. The call protocol runs on channel 0 exactly as on a top-levelalk/callconnection. Channel lifecycle operations (channel/open,channel/close,channel/control,channel/resources/subscribe) are call operations on channel 0'sOperationRegistry, gated by the existingAccessControl::check. No new auth machinery, no new framing. See ADR-036, ADR-037. -
The channels layer is a re-framing proxy, not a protocol engine. It converts between "one transport stream carrying N channels" (the wire) and "N independent
BiStreamhandles" (what handlers see). It does no ALPN-specific parsing, no auth, no transport coupling, and carries nostream_typeconcept (ADR-035). This makes it WASM-compatible and transport-agnostic by construction. See channels-adapter.md and ADR-039. -
The handler owns its sub-stream multiplexing. The channels layer yields one
BiStreamper channel; the handler sub-multiplexes it however it wants (TTY's 5-byte format, call's length-prefixed JSON, tunnel's raw bytes, SSH's channel protocol). The channels layer carries the bytes transparently. See channels-connection.md and ADR-035. -
channel/resources/subscribeis aSubscription, not a polledQuery. The call protocol hasStreamingHandler/invoke_streaming(ADR-021, implemented and tested). The first consumer (the hub aggregating worker resources) needs live updates. Polling would be built and immediately reworked. See ADR-037. -
Bidirectional open. Either side can open a channel to the other, just like the call protocol's operation overlay. The
directionfield onchannel/openpins who is the ALPN-server vs ALPN-client. See ADR-037 §Direction semantics. -
Wire-level invariants are contracts, not implementation details. The POC surfaced five invariants (REQ-CH-01..04, plus REQ-CH-06 for close ordering) that hang channels silently if underspecified: shutdown emits a zero-length sentinel; transport close drops all senders; the mux supports dynamic registration; unknown
channel_idis lenient-dropped; bounded-buffer backpressure doesn't deadlock; data chunks flush beforechannel/close. See channels-wire.md and channels-adapter.md. -
The hub translates, not transparently forwards. The hub terminates channel 0 on both legs, runs
AccessControl::check, and re-issueschannel/openon the spoke leg withforwarded_for(ADR-026). Data channels are byte-forwarded withchannel_idrewrite (a 4-byte rewrite within the 8-byte header). This preserves the auth model. See ADR-042. -
The channel cap is per-identity, not per-connection. A channel slot is a resource; the cap on how many an identity may hold open is a quota check, parallel to
OwnershipProvider::owns(ADR-011) for spawned resources. The cap lives inchannels-call(the channels layer is auth-blind by ADR-039 — no identity, no scopes), consulted by thechannel/openandchannel/closehandlers afterAccessControl::check. The default isPerIdentityChannelPolicy:: new(256)— 256 perPeerIdacross all the peer's connections. The per-connectionmax_channels(ADR-040) is a memory bound, not a DoS defense. The cap is symmetric (both sides enforce); the spoke caps the hub as direct caller, not the browser asforwarded_for(metadata, not authority — ADR-026). See ADR-041.
References
docs/research/alknet-channels/phase-0-findings.md— Phase 0 research (vision, hub motivation, wire format, negotiation, internals, DPs, OQs)docs/research/alknet-channels/poc-summary.md— the de-risk POC (28 tests, three validated targets, REQ-CH-01..06 wire-level invariants surfaced; REQ-CH-07 is a cosmetic clippy item, not a wire invariant)docs/research/alknet-channels/poc-plan.md— the POC plandocs/research/stream-unification/findings.md— the research that surfaced the pure-channel-multiplexing resolution (ADR-035)/workspace/alknet-channels-poc/— the POC codebasedocs/research/alknet-tty/phase-0-findings.md— the TTY crate's chunk format (the seed of the channels generalization)docs/research/alknet-ssh/phase-0-findings.md— SSH's channel multiplexer (the prior art for N-channel multiplexing)docs/architecture/crates/hub/README.md— the hub crate (the primary consumer; the relay implementation's home)