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.
159 lines
13 KiB
Markdown
159 lines
13 KiB
Markdown
---
|
|
status: draft
|
|
last_updated: 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](channels-overview.md) | draft | Crate purpose, the multiplexing collapse, dependencies, ALPN, transport agnosticism, WASM, relationship to existing crates |
|
|
| [channels-wire.md](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](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](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](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](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](decisions/034-channels-wire-format.md) | 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](decisions/035-channels-pure-channel-multiplexing.md) | channels Pure Channel Multiplexing | The umbrella decision: 8-byte header, no `stream_type`, `into_sub_streams` removed, `BiStream`-only, TTY always 5-byte |
|
|
| [036](decisions/036-channel-0-pre-negotiated-call.md) | Channel 0 Is Pre-Negotiated `alk/call` | Channel 0 = call protocol; no special control plane |
|
|
| [037](decisions/037-channel-lifecycle-operations.md) | Channel Lifecycle Operations on the Call Protocol | `channel/open`/`close`/`control`/`resources/subscribe`; `direction` semantics; subscribe not poll |
|
|
| [038](decisions/038-channelconnection-bidistreamsource.md) | ChannelConnection — BidiStreamSource over Chunk Reassembly | Per-channel `BidiStreamSource` impl; `accept_bi` yields `BiStream` (amended by ADR-035 — `into_sub_streams` removed) |
|
|
| [039](decisions/039-channelsadapter-and-channelmanager.md) | ChannelsAdapter and ChannelManager | Substrate-agnostic demux loop; REQ-CH-01..04 contracts |
|
|
| [040](decisions/040-backpressure-channel-limits-id-reuse.md) | 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](decisions/041-per-identity-channel-cap.md) | 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](decisions/050-pump-bidi-two-pump-helper.md) | `pump_bidi` — the Two-Pump Helper | The two-pump deadlock contract; handler-level, not channels-layer |
|
|
| [042](decisions/042-hub-relay-translate-not-forward.md) | Hub Relay — Translate, Not Transparently Forward | The hub translates channel 0, byte-forwards data channels with ID rewrite |
|
|
| [043](decisions/043-channelclient.md) | 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](decisions/044-channels-subcrate-decomposition.md) | 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](decisions/008-bidistreamsource-trait.md) | BidiStreamSource Trait | The `Connection` extension point `ChannelBidiStreamSource` implements |
|
|
| [009](decisions/009-bistream-as-the-handler-leaf.md) | `BiStream` as the Handler Leaf | `accept_bi` returns `BiStream`; the transport-leaf decision ADR-035 builds on |
|
|
| [007](decisions/007-connection-from-stream-generic-single-stream.md) | `Connection::from_stream` | The transport-agnostic `Connection` the channels layer rides on |
|
|
| [021](decisions/021-streaming-handler-for-subscriptions.md) | StreamingHandler for Subscriptions | The machinery `channel/resources/subscribe` uses |
|
|
| [026](decisions/026-forwarded-for-identity.md) | Forwarded-For Identity | The auth chain for hub-relayed channel opens |
|
|
| [031](decisions/031-crate-decomposition.md) | 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](/workspace/@alkdev/alknet/docs/architecture/decisions/077-tty-inside-channels.md): 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](/workspace/@alkdev/alknet/docs/architecture/decisions/052-alknet-tty-wire-format-and-two-carriage.md): 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](open-questions.md)"; 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](open-questions.md)"; 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](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](channels-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](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](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](channels-wire.md) and
|
|
[channels-adapter.md](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) |