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,137 @@
|
||||
# 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](067-websocket-carries-channels.md)),
|
||||
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](017-call-protocol-client-and-adapter-contract.md); record:
|
||||
alkcall ADR-022):
|
||||
|
||||
```rust
|
||||
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](015-privilege-model-and-authority-context.md),
|
||||
[ADR-022](022-handler-registration-provenance-and-composition-authority.md)).
|
||||
- **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](014-secret-material-flow-and-capability-injection.md)),
|
||||
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](067-websocket-carries-channels.md)); 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](../websocket.md) — the channels-over-WS session spec
|
||||
(framing shared with this adapter)
|
||||
- [ADR-067](067-websocket-carries-channels.md) — the server-side
|
||||
counterpart (upgrade path `/alk/channels`)
|
||||
- [ADR-017](017-call-protocol-client-and-adapter-contract.md) — the
|
||||
`OperationAdapter` contract
|
||||
- [ADR-014](014-secret-material-flow-and-capability-injection.md) —
|
||||
credential injection (the WSS bearer token path)
|
||||
- [ADR-048](048-websocket-native-session-not-gateway.md) — 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](../open-questions.md) OQ-01, OQ-03
|
||||
Reference in New Issue
Block a user