Skip to content

feat(dev/tunnel): WSS client + frame protocol (Phase 1)#20

Merged
I-am-nothing merged 2 commits into
mainfrom
feat/dev-wss-tunnel
May 5, 2026
Merged

feat(dev/tunnel): WSS client + frame protocol (Phase 1)#20
I-am-nothing merged 2 commits into
mainfrom
feat/dev-wss-tunnel

Conversation

@I-am-nothing

Copy link
Copy Markdown
Contributor

Summary

Adds the WebSocket-side building blocks for `mirrorstack dev`:

  • Frame envelope + type discriminators that mirror api-platform's `internal/dispatch/ws/frames.go`
  • `open()`: connects to `wss_url?token=ttok`, sends `register`, awaits `register_ack`
  • Background task: pings every 30s, replies to server pings, sends `close` frame on shutdown

Module is gated with `#[allow(dead_code)]` — it compiles + tests now but isn't wired into the dev lifecycle yet. End-to-end integration follows once two upstream pieces ship.

Wire-shape mirror

Both sides share the same envelope ordering and the same `type` discriminator strings. Reserved `FrameType::RpcReq` / `SqlReq` / etc. are enumerated client-side so future server frames of those shapes deserialize without forcing a reconnect once those planes go live.

Blocked on

  • mirrorstack-ai/api-platform#113 — server-side frame handling. This PR's Frame envelope mirrors that one.
  • mirrorstack-ai/api-platform AWS Sender impl + WS API GW IaC — without those, end-to-end is unreachable. Comment in `tunnel.rs` explains the integration follow-up.

Base branch

Stacks on `feat/dev-shell` (PR #19). Will rebase to main after #19 merges.

Deps added

Crate Features Why
`tokio` rt, macros, time, sync, net, io-util Async runtime for the WSS client only — the rest of the CLI stays blocking
`tokio-tungstenite` connect, rustls-tls-webpki-roots WSS dialer; webpki roots avoid OS cert store wiring on macOS/Windows
`futures-util` sink, std `SinkExt::send` / `StreamExt::next`

Test plan

  • `cargo test` — 4 new tests (round-trip serialize, type discriminator names, ID uniqueness, payload field naming)
  • `cargo clippy --all-targets -- -D warnings` clean
  • `cargo fmt --all -- --check` clean
  • After AWS Sender + IaC ships: end-to-end `mirrorstack dev --tunnel` against a deployed dispatch service

🤖 Generated with Claude Code

…n yet)

Adds the WebSocket side of `mirrorstack dev`:
  - Frame envelope (type, id, corr_id, stream_id, payload) — wire-shape
    matches api-platform's internal/dispatch/ws/frames.go
  - Connect: opens wss_url?token=ttok, sends register, awaits register_ack
  - Background task: pings every 30s, replies to server pings, sends
    close frame on shutdown signal

Deferred frame types (rpc.*, sql.*) are enumerated in the FrameType enum
so server frames of those shapes deserialize without forcing a reconnect
once those planes go live.

This module is `#[allow(dead_code)]` for now — it compiles + tests but
isn't wired into the dev lifecycle yet. End-to-end integration requires
two pieces still in flight elsewhere:
  - api-platform AWS Sender impl (apigatewaymanagementapi.PostToConnection)
  - IaC for the WS API GW + Lambda routing

Once those land, a small follow-up PR plugs this module into dev::run
and adds a --tunnel flag that gates the connect.

Deps added (used only inside this module):
  - tokio (rt + macros + time + sync + net + io-util)
  - tokio-tungstenite (connect + rustls-tls-webpki-roots)
  - futures-util (sink + std)

Test plan:
- [x] cargo test (4 new round-trip + uniqueness tests)
- [x] cargo clippy --all-targets -- -D warnings clean
- [x] cargo fmt --all -- --check clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@I-am-nothing I-am-nothing force-pushed the feat/dev-wss-tunnel branch from c59ae46 to 03dfe35 Compare May 5, 2026 17:42
@I-am-nothing I-am-nothing changed the base branch from feat/dev-shell to main May 5, 2026 17:42
@I-am-nothing I-am-nothing marked this pull request as ready for review May 5, 2026 17:42
…, observability

Five concrete fixes from the multi-agent review:

1. URL building uses url::Url + query_pairs_mut, matching auth::authorize_url
   (auth/mod.rs:52-61). Drops the brittle "?" vs "&" substring check.

2. Handshake flattened into await_register_ack(sink, stream, register_id) —
   removes three-level nesting (timeout > async > loop > match).

3. Background task surfaces transport failures via warn_prefix() instead of
   silently returning. Stream close, send error, server close all log
   actionable lines now. (Liveness-signal-on-handle queued as #29 — that's
   a structural change worth its own PR.)

4. WebSocketConfig caps inbound frames at 1 MiB (was: tungstenite default
   64 MiB). Per L1.5 a frame is at most 128 KB; 1 MiB gives streaming
   headroom without exposing a memory-bomb surface.

5. TunnelHandle drops the unused service_token field. RegisterAck still
   carries it (forward-compat with the wire shape); re-thread to the
   handle when the RPC plane lands and actually consumes it.

Plus: MissedTickBehavior::Delay on the heartbeat interval so a paused
runtime (laptop sleep) doesn't burst-ping on resume. Three new tests
cover the URL builder.

54 tests pass, clippy + fmt clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@I-am-nothing I-am-nothing merged commit 1cb6ca9 into main May 5, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant