docs: port architecture specs and ADRs from alknet-http; write new alkhttp ADRs 067-070
Phase 1 (SDD) — architecture documentation: Ported specs (adapted for alkcall, producer/consumer terms, 6-endpoint gateway, channels-over-WS, Sub/Pub operation types): - overview.md, http-server.md, http-adapters.md, http-mcp.md - README.md index (rewritten for alkhttp) New ADRs: - 067: WebSocket carries the channels protocol (8-byte chunk demux, channel 0 = alk/call, upgrade path /alk/channels) - 068: gateway /publish endpoint for Pub operations (NDJSON body) - 069: WebTransport out of scope in alkhttp (alknet concern) - 070: from_wss consumer adapter (wss feature, tokio-tungstenite) Ported ADRs (25, same numbers, port notes + amendments where the extraction changed facts): 001-004, 010, 014, 015, 017, 022, 023, 027, 034, 036, 037, 039, 041, 042, 044, 045, 046, 047, 048, 049, 051, 066. websocket.md rewritten for the channels session; open-questions.md seeded (OQ-01 WS byte-stream adapter, OQ-02 /publish framing, OQ-03 from_wss reconnect, OQ-04 browser client ownership). Verified: cargo test, clippy -D warnings, fmt, doc --no-deps.
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
# ADR-067: WebSocket Carries the Channels Protocol
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The alknet design (ADR-044, ADR-048 there) made the WebSocket path a
|
||||
bare call-protocol session: one `EventEnvelope` JSON object per binary
|
||||
WS message, handed directly to the shared `Dispatcher`. That design
|
||||
predates the channels protocol — the 8-byte chunk multiplexer (alkcall
|
||||
ADR-034/035) with channel 0 pre-negotiated as `alk/call` (alkcall
|
||||
ADR-036) — and was specified when browsers needed only the call
|
||||
protocol.
|
||||
|
||||
alkhttp is extracted onto alkcall, where the call protocol and the
|
||||
channels protocol are one crate, one connection model, and one wire
|
||||
story. Two problems remain with the bare-envelope WS design:
|
||||
|
||||
1. **No data channels for browsers.** A browser session over WS could
|
||||
reach the call protocol but could never open a data channel (TTY,
|
||||
tunnel, a WASM SSH client's transport). Every downstream protocol
|
||||
crate that rides channels would need a separate browser path.
|
||||
2. **Two connection shapes, one stack.** A browser WS session and a
|
||||
Rust in-line channels session (TCP+TLS) would be structurally
|
||||
different sessions at the dispatch layer: the browser one a raw
|
||||
envelope stream, the Rust one a channels connection with a
|
||||
`ChannelManager`. Hub code that wants to treat browser sessions and
|
||||
Rust spokes uniformly would branch on the transport.
|
||||
|
||||
## Decision
|
||||
|
||||
**The WebSocket path carries the channels protocol, not a bare
|
||||
envelope stream.** A WS session is an **in-line channels substrate**
|
||||
(alkcall ADR-034 §substrate modes): the WS connection's binary message
|
||||
stream is the transport; the 8-byte chunk header demultiplexes N
|
||||
logical channels over it; **channel 0 is pre-negotiated as `alk/call`**
|
||||
(alkcall ADR-036) and carries the native call-protocol session — the
|
||||
shared `Dispatcher` runs on it unchanged.
|
||||
|
||||
### Upgrade path
|
||||
|
||||
The default WS upgrade path is **`/alk/channels`** (was `/alknet/call`
|
||||
in the alknet design). The path is an axum route on the `HttpAdapter`
|
||||
router, subject to the same reserved-path collision rule as any
|
||||
default-surface route ([ADR-046](046-assembly-layer-custom-http-routes.md)).
|
||||
|
||||
### Framing: the WS message boundary carries chunks, not envelopes
|
||||
|
||||
The alknet design's "one `EventEnvelope` = one binary WS message, no
|
||||
length prefix" framing is **superseded**. The WS message boundary now
|
||||
carries channels chunks; the call protocol's envelopes ride inside
|
||||
channel 0 as length-prefixed JSON (alkcall ADR-014 frame format), the
|
||||
same as any other in-line channels transport.
|
||||
|
||||
Layering on the wire, for a call frame over WS:
|
||||
|
||||
```
|
||||
WS binary message
|
||||
└── chunk header [channel_id: u32 BE][length: u32 BE] (8 bytes)
|
||||
└── payload = frame [len: u32 BE][EventEnvelope JSON] (channel 0)
|
||||
```
|
||||
|
||||
A data channel's chunks are `chunk header + opaque payload` — the
|
||||
handler (TTY, tunnel, WASM client) owns its framing inside the payload,
|
||||
exactly as over TCP+TLS (alkcall ADR-035: no `stream_type`, the
|
||||
handler owns its sub-stream multiplexing).
|
||||
|
||||
The WS↔byte-stream adaptation (how the message-oriented WS stream
|
||||
presents as the byte stream the channels demux reads, and how the mux's
|
||||
byte writes become WS messages) is the implementation's core piece and
|
||||
its buffering semantics are tracked in OQ-01.
|
||||
|
||||
### Dispatch: channel 0 = the shared `Dispatcher`, unchanged
|
||||
|
||||
On upgrade, the handler:
|
||||
|
||||
1. Resolves the caller's identity from the `Authorization: Bearer`
|
||||
header via `IdentityProvider::resolve_from_token()` — the same auth
|
||||
path as any HTTP request ([ADR-004](004-auth-as-shared-core.md)).
|
||||
No token → `401`. The resolved identity is carried on the session
|
||||
for observability and `AccessControl`.
|
||||
2. Wraps the WS stream as a `Connection` (`Connection::from_bidi`, ALPN
|
||||
`alk/channels`).
|
||||
3. Runs the channels accept path — alkcall's `ChannelsAdapter`
|
||||
in-line demux loop — installing channel 0 via the
|
||||
`install_channel_zero` hook: construct channel 0's `CallConnection`
|
||||
and run `Dispatcher::run_loop_single_stream` on it, exactly as the
|
||||
TCP+TLS in-line substrate does.
|
||||
4. Data channels (1..N) are routed to whatever the deployment
|
||||
registered as openable ALPNs — for a hub, the same openable ALPNs a
|
||||
Rust spoke can reach. The browser opens them via the per-ALPN open
|
||||
ops on channel 0 (alkcall ADR-047), the same mechanism as any
|
||||
consumer.
|
||||
|
||||
Everything ADR-048 says about dispatch — `call.requested` →
|
||||
`Dispatcher::dispatch_requested` with `AccessControl::check` gating,
|
||||
`call.responded`/`call.completed`/`call.aborted` correlated by `id` via
|
||||
the pending map, text WS messages rejected with a protocol-level close
|
||||
— applies to channel 0 verbatim. The only framing change is the
|
||||
envelope's position in the layering (above).
|
||||
|
||||
### Bidirectionality, overlay, browsers-are-not-peers: unchanged
|
||||
|
||||
- **Both sides can initiate calls** on channel 0 (alkcall ADR-015's
|
||||
stream-agnostic correlation). The browser calls hub ops; the hub can
|
||||
call browser-registered ops over the same session.
|
||||
- **Browser-registered ops land in the connection-local Layer 2
|
||||
overlay** (alkcall ADR-019) and die when the WS connection drops.
|
||||
- **Browsers are not peers** ([ADR-034](034-outgoing-only-x509-and-three-peer-roles.md)
|
||||
§4): bearer token, no `PeerId`, not in the peer graph. The connection
|
||||
handle, not a `PeerRef`, is how the hub reaches browser ops.
|
||||
|
||||
### What this gives the browser
|
||||
|
||||
A browser session is now structurally the same session as a Rust
|
||||
in-line channels session:
|
||||
|
||||
- Call protocol operations (channel 0) — both directions.
|
||||
- Data channels for anything the deployment registers as openable —
|
||||
a WASM SSH client, a TTY, a tunnel — with no browser-specific
|
||||
protocol work.
|
||||
- The same discovery ops (`services/list`, `services/schema`) as any
|
||||
consumer.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
|
||||
- One session shape across browser and Rust in-line transports; hub
|
||||
code is transport-blind.
|
||||
- Browsers get data channels for free, riding alkcall's channels
|
||||
machinery rather than a bespoke browser protocol.
|
||||
- The `from_wss` consumer adapter ([ADR-070](070-from-wss-consumer-adapter.md))
|
||||
becomes the mirror image of the server path: the same WS byte-stream
|
||||
adaptation, then alkcall's `ChannelClient`.
|
||||
- The WS framing delta from alknet is contained to alkhttp's WS adapter;
|
||||
the Dispatcher, registry, and channels layers are untouched (they are
|
||||
alkcall's).
|
||||
|
||||
**Negative:**
|
||||
|
||||
- Browsers must speak the channels framing (8-byte header, channel 0
|
||||
open ops) rather than bare JSON envelopes. The JS client for this
|
||||
lives outside alkhttp; the framing is small and the alkcall BAST
|
||||
document (`chunk-header.bast.json`) is the cross-language contract.
|
||||
- The WS↔byte-stream adapter is new, unproven code with backpressure
|
||||
and partial-write hazards (OQ-01).
|
||||
- Existing alknet-era WS clients (bare envelope per message) break. No
|
||||
such client ships outside the mono-repo; the break is intentional and
|
||||
one-way.
|
||||
|
||||
## References
|
||||
|
||||
- [websocket.md](../websocket.md) — the full WS session spec
|
||||
- [ADR-048](048-websocket-native-session-not-gateway.md) — the native
|
||||
session (not gateway shape) decision this ADR amends (channel 0
|
||||
framing; upgrade path)
|
||||
- [ADR-044](044-defer-webtransport-browsers-use-websocket.md) — WS as
|
||||
the browser bidirectional path (stands); deferral mechanics
|
||||
superseded by [ADR-069](069-webtransport-out-of-scope.md)
|
||||
- [ADR-070](070-from-wss-consumer-adapter.md) — the consumer-side
|
||||
mirror of this decision
|
||||
- alkcall ADR-014 (EventEnvelope framing), ADR-015 (stream model),
|
||||
ADR-034/035 (channels wire format, pure multiplexing), ADR-036
|
||||
(channel 0 pre-negotiated `alk/call`), ADR-039 (ChannelsAdapter),
|
||||
ADR-043 (ChannelClient), ADR-047 (openable ALPNs are operations)
|
||||
- alkcall `docs/architecture/channels-wire.md` — the 8-byte chunk
|
||||
format and wire invariants
|
||||
Reference in New Issue
Block a user