--- 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).