Skip to content

feat(dev): wire WSS tunnel into mirrorstack dev (--tunnel)#22

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

feat(dev): wire WSS tunnel into mirrorstack dev (--tunnel)#22
I-am-nothing merged 2 commits into
mainfrom
feat/dev-tunnel-integration

Conversation

@I-am-nothing

Copy link
Copy Markdown
Contributor

Summary

Final CLI piece for Phase 1. `mirrorstack dev --tunnel` now drives the full handshake against the dispatch service:

```
$ mirrorstack dev --tunnel
✓ fetching tunnel token from http://localhost:8083
✓ tunnel registered (session conn_abc...)
✓ bootstrapped docker-compose.yml
[+] Running 2/2
✓ module running — Ctrl-C to stop
module │ mirrorstack: my-mod module (My Mod) listening on :8080
^C
✓ tunnel closed
✓ docker compose down
```

Wiring

  1. Loads credentials (errors with login hint if expired)
  2. POSTs `/v1/tunnel/token` → `{token, wss_url, expires_at}`
  3. Parses `Config.ID` out of main.go for the module identity
  4. Opens WSS, sends `register`, awaits `register_ack` (10s timeout)
  5. Holds the handle for the dev session's lifetime
  6. On Ctrl-C / module exit: shutdown signal → close frame → drop conn

Tunnel comes up before compose so a registration failure doesn't waste the user's time bringing containers up first.

Notable choices

  • `new_multi_thread` runtime (one worker), not `new_current_thread`. The pinger / inbound-frame reader is a spawned task; current-thread runtimes only tick during `block_on`, which would freeze the spawned future the moment connect returned.
  • Module identity comes from parsing `Config.ID = "..."` out of main.go — the canonical SDK value, also what the scaffold substituted from the platform UUID. Future PR can move to `.mirrorstack/meta.json` written at scaffold time.
  • New `--local-url` flag (default `http://localhost:8080\`) tells dispatch where to 302 inbound Leaf 1 calls. SDK listener port is configurable, so the flag stays.
  • New `MIRRORSTACK_DISPATCH_URL` env (default `https://api.mirrorstack.ai\` per ingress path-routing convention) overrides the dispatch base.

Blocked on

Deps added

  • `tokio.rt-multi-thread` feature flag (no new crate, just enabling a feature on the existing tokio dep)

Test plan

  • `cargo test` clean — 43 tests (5 new `module_meta` tests)
  • `cargo clippy --all-targets -- -D warnings` clean
  • `cargo fmt --all -- --check` clean
  • After WS API GW IaC ships: `mirrorstack dev --tunnel`, confirm `register_ack` arrives and `/m/<module_id>/foo` from a browser 302s to localhost

🤖 Generated with Claude Code

Closes the CLI-side gap. \`mirrorstack dev --tunnel\` now:
  1. Loads credentials (errors out with login hint if expired)
  2. POSTs /v1/tunnel/token to mirrorstack-dispatch → ttok + wss_url
  3. Parses Config.ID out of main.go (the canonical module id)
  4. Opens WSS, sends register {module_id, local_url, version}, awaits
     register_ack with a 10s timeout
  5. Holds the tunnel handle for the lifetime of the dev session
  6. On Ctrl-C / module exit: shutdown signal → close frame → drop conn

The tunnel comes up BEFORE compose so a registration failure doesn't
waste the user's time bringing containers up first.

Notable wiring choices:

- \`new_multi_thread\` runtime with one worker, not \`new_current_thread\`.
  The pinger / inbound-frame reader is a spawned task; current-thread
  runtimes only tick during \`block_on\`, which would freeze the spawned
  future the moment the connect call returned.

- Module identity comes from parsing \`Config.ID = "..."\` out of main.go.
  That's the same value the SDK uses for \`mod_<id>\` schemas, and what
  the CLI scaffold substituted from the platform UUID. A future PR can
  switch to a \`.mirrorstack/meta.json\` written at scaffold time; for
  now, parsing main.go is a one-line regex on a file whose shape we
  control.

- New \`--local-url\` flag (default http://localhost:8080) tells dispatch
  where to 302 incoming Leaf 1 calls. Most modules will run there, but
  the SDK's HTTP listener is configurable, so the flag stays.

- New \`MIRRORSTACK_DISPATCH_URL\` env (default https://api.mirrorstack.ai
  per ingress path-routing convention) overrides the dispatch base.

Adds \`tokio.rt-multi-thread\` feature; otherwise no new deps.

Test plan:
- [x] cargo test (43 pass; 5 new module_meta tests)
- [x] cargo clippy --all-targets -- -D warnings clean
- [x] cargo fmt --all -- --check clean
- [ ] After WS API GW IaC ships: \`mirrorstack dev --tunnel\` against a
      deployed dispatch service, confirm register_ack arrives + browser
      hits to /m/<module_id>/foo 302 to localhost

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…T_LOCAL_URL

Two fixes from the multi-agent review:

1. Promote `session_expired()` from commands/module/mod.rs to commands/
   mod.rs as `pub(crate)`, so dev::open_tunnel and module::init route
   their 401-handling through the same helper. Without this they had
   the exact same string inline in two places — accidental duplication
   that would drift the moment one is reworded.

2. Promote the magic `"http://localhost:8080"` literal to a
   `DEFAULT_LOCAL_URL` const next to `DEFAULT_DB_URL`. Used at one site
   today (the --local-url flag's fallback in open_tunnel), but the
   value was duplicated in the arg's doc string, so a future change
   would have three places to update. One source of truth now.

Skipped from the review:
- The 200ms post-shutdown sleep — flagged as worth replacing with an
  awaited shutdown future, but TunnelHandle::shutdown is fire-and-
  forget by design and reshaping it is part of issue #29's liveness
  signal work.
- module_meta::extract_id leading-comment edge case — covered by the
  existing test naming and not worth tightening for v1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@I-am-nothing I-am-nothing merged commit 5e4ceb5 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