Files
alkcall/docs/architecture/channels-README.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

13 KiB

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

alkcall — Channels Protocol

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 + from_source, 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-050)
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 the composition layer's dial helper (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)
050 pump_bidi — the Two-Pump Helper 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 the composition layer's dial helper (ADR-045)
044 channels Sub-Crate Decomposition channels-core (pure multiplexer) / channels-call (call coupling + ChannelClient) modules inside alkcall; 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
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 The channels subsystem lives inside alkcall with the call protocol and vendored core types; no handler-depends-on-handler

Provenance (source-repo ADRs, not ported): alknet ADR-077 — 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. alknet ADR-052 — TTY Wire Format: the 5-byte format carried transparently in the channels payload (control bidirectional via STREAM_CTRL_IN/OUT — Phase 7 amendment). TTY is now a sibling protocol crate (alktty).

Relevant Open Questions

OQ Title Status Relevance
OQ-23 ("tracker"; source: 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"
Dial seam (source: alknet OQ-55) Client dial helper extraction resolved (ADR-045); ownership → OQ-43 ChannelClient's API is decided (ADR-043): transport-agnostic from_connection primary; the dial lives in the composition layer's dial helper (AlknetClient today; post-alknet owner and shape — OQ-43)
Two-pump helper (source: alknet OQ-57) Two-pump helper extraction to a shared core crate dissolved The shutdown-on-completion contract is decided and the helper is in-tree (channels::pump_bidi, ADR-050); the extraction target (a mono-repo core crate being replaced) no longer exists — alkcall owns its helpers
OQ-24 ("tracker"; source: 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 shipped surface integrates the strip (see OQ-24 in open-questions.md)

Key Design Principles

  1. Streams are streams. A TTY session, an SSH channel, a forwarded TCP connection, a QUIC bidi stream — they're all BiStream (a concrete AsyncRead + AsyncWrite newtype, per ADR-009). The differences are only in how they're opened (negotiation via channel/open on channel 0) and what multiplexing layer carries them (the 8-byte chunk format). Once normalized, every channel is an ALPN routed through the same HandlerRegistry. See overview.md and ADR-034 (as amended by ADR-035).

  2. Channel 0 is alk/call pre-negotiated, not a special control plane. The call protocol runs on channel 0 exactly as on a top-level alk/call connection. Channel lifecycle operations (channel/open, channel/close, channel/control, channel/resources/subscribe) are call operations on channel 0's OperationRegistry, gated by the existing AccessControl::check. No new auth machinery, no new framing. See ADR-036, ADR-037.

  3. 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 BiStream handles" (what handlers see). It does no ALPN-specific parsing, no auth, no transport coupling, and carries no stream_type concept (ADR-035). This makes it WASM-compatible and transport-agnostic by construction. See channels-adapter.md and ADR-039.

  4. The handler owns its sub-stream multiplexing. The channels layer yields one BiStream per 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.

  5. channel/resources/subscribe is a Subscription, not a polled Query. The call protocol has StreamingHandler / 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.

  6. Bidirectional open. Either side can open a channel to the other, just like the call protocol's operation overlay. The direction field on channel/open pins who is the ALPN-server vs ALPN-client. See ADR-037 §Direction semantics.

  7. 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_id is lenient-dropped; bounded-buffer backpressure doesn't deadlock; data chunks flush before channel/close. See channels-wire.md and channels-adapter.md.

  8. The hub translates, not transparently forwards. The hub terminates channel 0 on both legs, runs AccessControl::check, and re-issues channel/open on the spoke leg with forwarded_for (ADR-026). Data channels are byte-forwarded with channel_id rewrite (a 4-byte rewrite within the 8-byte header). This preserves the auth model. See ADR-042.

  9. 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 in channels-call (the channels layer is auth-blind by ADR-039 — no identity, no scopes), consulted by the channel/open and channel/close handlers after AccessControl::check. The default is PerIdentityChannelPolicy:: new(256) — 256 per PeerId across all the peer's connections. The per-connection max_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 as forwarded_for (metadata, not authority — ADR-026). See ADR-041.

References

Source-repo research and history (alknet, provenance — these paths live in the alknet repo, /workspace/@alkdev/alknet/docs/research/..., not this one):

  • 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 plan
  • docs/research/stream-unification/findings.md — the research that surfaced the pure-channel-multiplexing resolution (ADR-035)
  • /workspace/alknet-channels-poc/ — the POC codebase
  • docs/research/alknet-tty/phase-0-findings.md — the TTY crate's chunk format (the seed of the channels generalization; TTY is now the sibling crate alktty)
  • docs/research/alknet-ssh/phase-0-findings.md — SSH's channel multiplexer (the prior art for N-channel multiplexing)
  • Source-repo hub crate (alknet, provenance — the composition layer's hub wiring; the relay implementation itself is an alkcall export, ChannelRelay, ADR-051)