Files
alkhttp/docs/architecture/decisions/070-from-wss-consumer-adapter.md
T
glm-5.3-flash 320ea87b08 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.
2026-08-27 14:19:24 +00:00

6.0 KiB

ADR-070: from_wss — the WSS Consumer Adapter

Status

Accepted

Context

The alknet design named from_wss as a future, out-of-scope adapter (alknet websocket.md §"Future"): importing a remote alk node's operations over a WebSocket connection, mirroring from_call's pattern with WSS as the transport. It was deferred there because no concrete consumer existed.

Extraction changes the calculus. The browser path is now channels-over-WebSocket (ADR-067), which means:

  1. The server half of WS-channels is being built anyway. The consumer half (ChannelClient-over-WS) is the mirror image sharing the same WS↔byte-stream adapter (OQ-01) — building it later means re-validating the adapter independently.
  2. A deployment pattern became concrete: a Rust process behind a restrictive network (outbound-only HTTPS/WSS allowed, no QUIC reachability) that needs to consume another node's operations. QUIC (from_call over an alk/call connection) and TCP+TLS (alk/channels) both require reachability the deployment may not have; WSS rides standard HTTPS infrastructure.
  3. alkcall provides both halves. ChannelClient (alkcall ADR-043) is the channels consumer; from_call (alkcall ADR-028) is the same-protocol importer pattern. from_wss composes them over a WSS transport.

Decision

alkhttp ships from_wss: a consumer adapter that connects to a remote node's WSS endpoint, runs the channels-over-WS session, and imports the remote node's operations as forwarding handlers — the same-protocol importer, with WSS as the transport instead of QUIC.

  • Feature gate: wss = ["dep:tokio-tungstenite"] (not default — a process that never consumes over WSS should not compile a WS client).

  • Shape: implements OperationAdapter (ADR-017; record: alkcall ADR-022):

    pub struct FromWss {
        endpoint: url::Url,          // wss://host/alk/channels
        auth_token: Option<String>,  // injected via Capabilities at registration
        namespace: Option<String>,   // local-naming sugar, as from_call's namespace_prefix
    }
    
    #[async_trait]
    impl OperationAdapter for FromWss {
        async fn import(&self) -> Result<Vec<HandlerRegistration>, AdapterError>;
    }
    
  • Import flow: dial WSS → wrap the tungstenite stream as a Connection (Connection::from_bidi, ALPN alk/channels) → run the consumer side of the channels session (alkcall ChannelClient machinery: install channel 0, run the client dispatch loop) → services/list + services/schema over channel 0 → one forwarding HandlerRegistration per discovered op (provenance FromCall, leaf, Internal by default — ADR-015, ADR-022).

  • Forwarding at call time: each imported op's handler serializes the call input as a call.requested frame, writes it (length-prefixed JSON, alkcall ADR-014) into channel 0's write half, and correlates call.responded/call.completed/call.aborted by id via the pending map — identical to from_call's forwarding shape, because the protocol is the same protocol.

  • Auth: the WSS upgrade request carries Authorization: Bearer <token>; the token comes from OperationContext.capabilities at handler construction (the no-env-vars path — ADR-014), never from std::env::var.

  • Relationship to the server path: the WS↔byte-stream adapter is shared with the server-side upgrade handler (OQ-01) — one implementation, used in both directions. The session itself is alkcall's (ChannelClient consumer half + channel 0 dispatch loop), not forked.

Not in scope

  • Reconnection policy semantics (auto-reconnect, stale registration invalidation): tracked in OQ-03; v1 surfaces a connection-drop as imported-op call failures (INTERNAL, retryable) and leaves policy to the assembly layer.
  • Serving WSS from the consumer side — the producer side of a WSS session is the HttpAdapter upgrade route (ADR-067); no separate server type.
  • from_wss over non-TLS ws:// — plaintext WS is allowed by the underlying transport for local/test use but is not the adapter's documented path.

Consequences

Positive:

  • Consumers behind outbound-only HTTPS get the full operation surface of a remote node without QUIC/TCP reachability.
  • The WS↔byte-stream adapter is validated in both directions by construction (server upgrade path + consumer path share it).
  • Same-protocol import means zero translation: the remote node's ops appear in the local registry with their real specs and error schemas.

Negative:

  • One more WS client dependency in the tree when wss is enabled (tokio-tungstenite).
  • Long-lived WSS connections behind proxies/load balancers need keepalive/timeout tuning — deployment concern, but worth documenting.
  • Reconnection semantics are initially minimal (OQ-03); a consumer wanting hot re-registration must wait for or build the policy.

References

  • websocket.md — the channels-over-WS session spec (framing shared with this adapter)
  • ADR-067 — the server-side counterpart (upgrade path /alk/channels)
  • ADR-017 — the OperationAdapter contract
  • ADR-014 — credential injection (the WSS bearer token path)
  • ADR-048 — why the consumer speaks the native session, not the gateway shape
  • alkcall ADR-022 (adapter contract), ADR-028 (from_call — the same-protocol importer pattern this mirrors), ADR-043 (ChannelClient), ADR-014 (framing), ADR-034/035 (channels wire)
  • open-questions.md OQ-01, OQ-03