- FromWss: dial wss:// -> split_tungstenite_to_bytes (client-side twin of the axum WS byte-adapter; one seam, both directions, OQ-01) -> Connection::from_bidi(b"alk/channels") -> alkcall ChannelClient (channel 0 install + dispatch loop) -> alkcall from_call importer. No protocol fork: specs mirror the remote, provenance FromCall. - Drop semantics (OQ-03 v1): session drop -> monitor fails all in-flight pendings retryable CONNECTION_CLOSED (WsPumps::read_eof Notify); no 30s-deadline hang. - Bearer token via constructor/assembly layer (ADR-014 no-env-vars). Production fix in the WS server half (upgrade.rs): the upgrade identity now propagates to channel 0's CallConnection (was AuthContext::anonymous -> dispatcher saw no identity, ACL checks ran unauthenticated; services/list filtered scoped ops for all callers). 9 in-module tests incl. full round-trip consumer<->server (both halves of the adapter together), ACL end-to-end, drop-no-hang. Verified: cargo test (227 lib default), --all-features (227 lib + 5 MCP + 10 WS integration), clippy -D warnings (both), fmt.
91 lines
4.1 KiB
Markdown
91 lines
4.1 KiB
Markdown
---
|
|
id: adapter-from-wss
|
|
name: from_wss consumer adapter (wss feature)
|
|
status: completed
|
|
depends_on: [ws-byte-adapter, ws-upgrade-session]
|
|
scope: moderate
|
|
risk: high
|
|
impact: component
|
|
level: implementation
|
|
tags: [adapters, phase-3]
|
|
---
|
|
|
|
## Description
|
|
|
|
Implement `from_wss` per ADR-070 behind the `wss` feature
|
|
(tokio-tungstenite): dial wss:// endpoint → adapt the WS stream with
|
|
the shared byte-adapter → `Connection::from_bidi(_, b"alk/channels")` →
|
|
consumer half (alkcall ChannelClient machinery: channel 0 install +
|
|
client dispatch loop) → services/list + services/schema → one
|
|
forwarding HandlerRegistration per discovered op (FromCall provenance,
|
|
leaf, Internal default). Forwarding: serialize input → call.requested
|
|
frame on channel 0 → correlate by id. Bearer token from Capabilities
|
|
(no-env-vars). Reconnect policy: v1 = drop → retryable failures
|
|
(OQ-03 disposition).
|
|
|
|
## Acceptance Criteria
|
|
|
|
- [x] Adapter behind `wss` feature; base crate compiles without it
|
|
- [x] Round-trip test: from_wss consumer ↔ ws-upgrade-session server (both halves of the adapter exercised together)
|
|
- [x] Discovered ops invoke correctly; identity/ACL enforced end-to-end
|
|
- [x] Connection drop → in-flight calls fail retryable, no hang
|
|
- [x] Credentials flow from Capabilities only (no env-var reads)
|
|
- [x] `cargo test --all-features` passes
|
|
|
|
## References
|
|
|
|
- docs/architecture/decisions/070-from-wss-consumer-adapter.md
|
|
- docs/architecture/websocket.md (§The consumer-side mirror)
|
|
- alkcall ADR-028 (from_call pattern), ADR-043 (ChannelClient)
|
|
|
|
## Notes
|
|
|
|
Two production fixes surfaced by the round-trip tests:
|
|
|
|
1. **WS identity now propagates to channel 0** (src/websocket/
|
|
upgrade.rs): ChannelsAdapter constructs channel 0's `Connection`
|
|
fresh, so the identity set on the channels-layer connection never
|
|
reached the dispatcher. install_channel_zero now sets
|
|
auth.identity on the channel-0 connection before accept_bi, and
|
|
run_channels_session passes the upgrade identity via AuthContext
|
|
(was AuthContext::anonymous — identity: None). Without this,
|
|
services/list ACL-filtered everything requiring scopes and every
|
|
ACL check ran unauthenticated.
|
|
2. **Connection-drop monitor**: alkcall's client-side read pump
|
|
(read_single_stream_until_closed) routes envelopes but does not
|
|
fail pendings on EOF — fail_all lives in the dispatcher loops
|
|
only. from_wss owns drop semantics per OQ-03 v1: the byte adapter
|
|
exposes WsPumps::read_eof() (Notify fired on WS read EOF), a
|
|
monitor selects on it + the session close signal and calls
|
|
fail_all(CONNECTION_CLOSED, retryable) on channel 0's pending map.
|
|
|
|
## Summary
|
|
|
|
Implemented `from_wss` per ADR-070 behind the `wss` feature:
|
|
|
|
- `src/adapters/from_wss.rs`: FromWss (endpoint + optional bearer
|
|
token + optional namespace prefix). WssSession::connect dials the
|
|
WSS endpoint (Authorization: Bearer from the constructor token —
|
|
assembly layer passes it from Capabilities; no env reads), adapts
|
|
the tungstenite socket via the new
|
|
`split_tungstenite_to_bytes` (the client-side twin of the axum
|
|
server-side split — same chunk framing, same 1002 text-rejection,
|
|
same EOF mapping; one seam, both directions, OQ-01), builds
|
|
Connection::from_bidi(b"alk/channels"), runs alkcall
|
|
ChannelClient (channel 0 install + demux/mux/read-pump), then
|
|
reuses alkcall's from_call importer (services/list +
|
|
services/schema + build_bundles) — no protocol fork (AGENTS.md
|
|
convention 9). Provenance FromCall, leaf, Internal-by-default is
|
|
the assembly layer's option; specs mirror the remote (ADR-017 §3).
|
|
- Drop semantics (OQ-03 v1): WssSession drop → monitor fail_all
|
|
retryable CONNECTION_CLOSED; in-flight calls resolve promptly, no
|
|
30s-deadline hang.
|
|
- 9 in-module tests: round-trip against the real server half
|
|
(ws-upgrade-session axum server): discovery with admin-scope and
|
|
unprivileged identities (ACL end-to-end), namespace prefix, echo
|
|
invocation end-to-end, drop-no-hang, transport error
|
|
classification, no-env-vars.
|
|
|
|
227 lib (default) / 227 lib (all-features) + 5 MCP + 10 WS
|
|
integration green. clippy -D warnings both feature sets, fmt.
|
|
Default build has no tungstenite client path (feature-gated). |