Commit Graph
1 Commits
Author SHA1 Message Date
Palash DebnathandClaude Fable 5 22ba348f17 feat(remote): backend URL + bearer key + Tailscale docs (Wave 2.3) (#364)
Run inference on a remote GPU box, drive it from the desktop app — opt-in,
off by default (loopback-only is unchanged when no key is set).

Backend:
- BearerKeyMiddleware (main.py): when OMNIVOICE_API_KEY is set, every
  non-loopback HTTP + WebSocket request must present it (Authorization:
  Bearer, ?api_key=, or the ov_key cookie set on first auth). Pure ASGI
  (no response buffering), loopback always bypasses, SPA shell stays
  reachable. Constant-time compare, never logged.
- ws_remote_authorized() in dependencies; capture_ws lets a keyed
  non-loopback client through its inline loopback guard (the thin-client
  dictation case: mic local, GPU remote).

Frontend:
- api/client.ts: ov_backend_url (localStorage) is the top-precedence base
  override; new wsUrl() derives ws scheme + host from the API base (not
  window.location, which lies in the Tauri webview) and appends ?api_key.
  apiFetch attaches the bearer header. Both WS call sites (dictation,
  events) routed through wsUrl; the HTTP transcribe fallback through
  apiFetch.
- Settings > Sharing > Remote backend panel: URL + key fields, a
  test-connection probe against {url}/health, save-and-reload.

Docs: docs/remote-gpu.md — the Tailscale recipe (MagicDNS + Serve, never
Funnel, headscale note, plain-HTTP-is-sniffable warning, PIN-vs-key split).

Tests: 10 bearer-middleware cases (inert without env, loopback bypass,
401 without/pass with key via header+query, wrong key, shell exemption,
plain-ASGI guard, WS handshake reject/accept). Validated in CI.

Spec: parity program Wave 2.3 / competitive-analysis §R2 rungs 1-3.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 03:57:20 +05:30