Files
alkcall/docs/architecture/decisions/036-channel-0-pre-negotiated-call.md
glm-5.2 cc470a363a docs: port architecture specs + 45 ADRs from alknet, renumbered
Port the call + channels architecture documentation from the alknet
mono-repo into docs/architecture/, renumbered as alkcall ADR-001..045.

Renumbering map (alknet -> alkcall):
  Core:        001,002,004,006,007,011,065,070,092,014,050,091 -> 001-012
  Call:        005,064,012,023,015,022,024,016,049,017,028,029,030,032,066,069,067,068 -> 013-030
  Shared:      003,009,013 -> 031-033
  Channels:    071,093,072,073,074,075,076,094,079,080,081,089 -> 034-045

3 superseded/reversed ADRs kept for historical trail:
  - ADR-013 (irpc foundation, superseded by ADR-014)
  - ADR-023 (peer-scoped filtering, superseded by ADR-024)
  - ADR-077 (TTY inside channels, reversed by ADR-035 — not ported, TTY-only)

Ported docs (11 spec files + README + open-questions):
  - call-README.md, call-protocol.md, operation-registry.md, client-and-adapters.md
  - channels-README.md, channels-overview.md, channels-wire.md, channels-connection.md, channels-adapter.md, channel-operations.md, channel-client.md
  - README.md (index with doc table, ADR table grouped by category, key principles)
  - open-questions.md (lean — 30 OQs, renumbered OQ-01..030; includes new OQ-22 for the pub/sub gap)

Cross-reference rewriting:
  - All ADR-NNN references rewritten single-pass (no chaining bug)
  - Markdown link paths fixed
  - Title lines aligned with filenames
  - Non-ported ADR refs (052, 082, 086, etc.) left as-is with README note

The open-questions.md includes OQ-22 (new): the call protocol pub/sub
gap — subscribe exists but pub does not, needed for channels
channel/resources/subscribe fan-out. This is the next ADR to write
(alkcall ADR-046).
2026-08-12 07:06:57 +00:00

156 lines
7.7 KiB
Markdown

# ADR-036: Channel 0 Is Pre-Negotiated `alknet/call`
## Status
Accepted (amended 2026-07-18 by ADR-035 — channel 0's `stream_types`
field is removed; the channels layer has no `stream_type` concept; the
call protocol's `EventEnvelope` framing is the channels payload, carried
transparently — see "Amendment (ADR-035, 2026-07-18)" below)
## Amendment (ADR-035, 2026-07-18)
Channel 0's `stream_types` field (the `[0, 1]` active set) is **removed**.
The channels layer has no `stream_type` concept (ADR-035) — it carries the
call protocol's `EventEnvelope` framing (ADR-014) transparently in the
8-byte header's payload. The call protocol's bidirectionality (client
writes requests, server writes responses) is a call-protocol concern,
not a channels-layer concern; the channels layer routes by `channel_id`
only and yields a `BiStream` to the `CallAdapter`. The `CallAdapter`'s
`accept_bi()` returns one `BiStream` (per ADR-009); the call protocol
reads/writes `EventEnvelope` frames on it, exactly as on a top-level
`alknet/call` connection.
The body below describes the **original** (with `stream_types`) shape;
the amendment above is the operative decision. See ADR-035 for the
resolution rationale and the cross-ADR impacts.
## Context
A channels connection carries N logical channels. One of them must carry the
call protocol — the JSON-RPC layer that orchestrates channel lifecycle
(`channel/open`, `channel/close`, `channel/control`, `channel/resources`).
The question is how channel 0 relates to the call protocol: is it a special
"control plane" with its own framing, or is it just `alknet/call` pre-
negotiated?
The phase-0 research (`docs/research/alknet-channels/phase-0-findings.md`
§DP-2) recommends channel 0 is `alknet/call` pre-negotiated — no special
framing, no separate control-plane wire format. The call protocol runs on
channel 0 exactly as it runs on a top-level `alknet/call` QUIC connection.
This matters because the alternative (a special control plane) would mean
the channels layer has its own JSON protocol for channel lifecycle, parallel
to and duplicating the call protocol's `OperationRegistry`, `AccessControl`,
`OperationContext`, and `forwarded_for` machinery. That duplication is the
"re-implement every protocol's framing per transport" problem the hub
motivation (§Hub Motivation) identifies as the thing channels exists to
collapse.
## Decision
**Channel 0 is `alknet/call`, pre-negotiated.** Both sides of a channels
connection know that `channel_id = 0` is routed to the `CallAdapter` without
an explicit `channel/open` exchange. The `CallAdapter` receives a
`Connection` backed by channel-0 chunk reassembly and dispatches operations
exactly as it does on a top-level `alknet/call` connection.
### What this means concretely
1. **Channel 0 uses the same 9-byte chunk format as every other channel**
(ADR-034). Its chunks have `channel_id = 0` in the header. No special
first-byte trick, no separate framing.
2. **The `CallAdapter` is unchanged.** It receives a `Connection`, calls
`accept_bi()`, gets one bidi stream (the channel-0 reassembled stream),
and runs its dispatch loop. `EventEnvelope` frames ride on `stream_type =
0` of channel 0. The `CallAdapter` does not know it is inside a channels
connection.
3. **Channel lifecycle operations are call operations.** `channel/open`,
`channel/close`, `channel/control`, `channel/resources` are registered on
the call protocol's `OperationRegistry` at assembly time (ADR-037). They
are dispatched through the existing `OperationContext` (identity, scopes,
capabilities, ownership, `forwarded_for`), gated by the existing
`AccessControl::check`. No new auth machinery, no new framing, no
protocol version bump.
4. **Channel 0 is allocated at `ChannelsAdapter::handle` entry.** The
`ChannelsAdapter` constructs channel 0's reassembly buffers, wraps them
as a `Connection` (via `Connection::from_source` with a
`ChannelBidiStreamSource` — ADR-008/074), and hands that `Connection` to
the `CallAdapter` — exactly as if `alknet/call` had been the top-level
ALPN. The `CallAdapter` is looked up in the same `HandlerRegistry` as
every other ALPN.
### Channel 0's stream_type usage
| stream_type | direction | purpose |
|-------------|-----------|---------|
| 0 | write (client→server) | `EventEnvelope` frames from the client (call.requested, call.aborted) |
| 1 | read (server→client) | `EventEnvelope` frames from the server (call.responded, call.completed, call.error) |
Channel 0 uses stream_types [0, 1] — the call protocol is bidirectional via
two unidirectional halves, the same way every channel type works
(ADR-034 §stream_type decomposition). The call protocol's `(SendStream,
RecvStream)` pair maps directly: `SendStream` backed by stream_type 0,
`RecvStream` backed by stream_type 1. Both sides write to their write half
and read from their read half — no shared stream_type both sides write to.
The call protocol is JSON-only and single-stream by design (ADR-014).
stream_types 2-255 on channel 0 are reserved for future call-protocol
sub-streams.
## Consequences
**Positive:**
- No control-plane duplication. The channels layer reuses the call protocol's
`OperationRegistry`, `AccessControl`, `OperationContext`, `forwarded_for`,
and `StreamingHandler` (ADR-021) machinery verbatim. Channel lifecycle is
just another class of call operations.
- The `CallAdapter` is transport-agnostic by construction — it works
identically whether the `Connection` is a top-level QUIC stream or a
channels-reassembled channel-0 stream. This is the "streams are streams"
insight made concrete.
- `channel/resources/subscribe` (ADR-037) is a `Subscription` operation on
channel 0, using the already-implemented `StreamingHandler` /
`invoke_streaming` path (ADR-021). The resource registry is a live view,
not a polled snapshot.
- Auth is inherited: `channel/open` goes through `AccessControl::check`
exactly like any other call operation. The channels layer does not re-
implement auth.
**Negative:**
- Channel 0 is a single point of orchestration. If channel 0's `CallAdapter`
hangs, no new channels can be opened. This is the same property as the call
protocol today (one dispatch loop per connection) and is not a new
vulnerability.
- The call protocol's JSON-only nature means channel lifecycle operations
are JSON. For high-frequency control (e.g., per-keystroke resize), this is
more overhead than a binary control frame. The division (ADR-037 §DP-4)
handles this: `stream_type 3` on the data channel for data-ordered control,
call operations for lifecycle and infrequent control.
## Door type
**One-way.** Channel 0's role as `alknet/call` pre-negotiated is a wire-
format and protocol-structure commitment. Changing it after deployments
exist (e.g., to a special control plane) requires a version migration and
re-architecting the channel lifecycle operations. The reservation of
`stream_type` 1-255 on channel 0 is a two-way-door detail (they're currently
unused; assigning them is additive).
## References
- ADR-034: channels wire format (the 8-byte chunk header channel 0 uses,
as amended by ADR-035)
- ADR-035: channels pure channel multiplexing (amends this ADR —
channel 0's `stream_types` field removed; the call protocol's framing
is the channels payload, carried transparently)
- ADR-037: channel lifecycle operations (registered on channel 0's
`OperationRegistry`)
- ADR-014: irpc never integrated — hand-rolled EventEnvelope framing (the
call protocol channel 0 carries)
- ADR-021: StreamingHandler for subscriptions (the machinery
`channel/resources/subscribe` uses)
- ADR-008: BidiStreamSource trait (the `Connection` extension point)
- `docs/research/alknet-channels/phase-0-findings.md` §DP-2, §Channel 0