Skip to content

feat(mcp): live review window — watch a shared session build, drop pins - #312

Merged
ecto merged 3 commits into
mainfrom
claude/live-window-app
Jun 23, 2026
Merged

ecto merged 3 commits into
mainfrom
claude/live-window-app

Conversation

@ecto

@ecto ecto commented Jun 23, 2026

Copy link
Copy Markdown
Owner

What & why

The flagship of Track A: a "watch Claude build, live" review window. The backbone (event spine + /live routes) shipped in #310/#311; this adds the experience — share a session as a link, watch geometry update in real time, and drop annotation pins — and makes it private by default.

Paste mcp.vcad.io/live/<id> → see the model build, who else is watching, and pin feedback onto it.

Commits

Commit What
8e767db0 A1 — share gate + geometry endpoint: live_shares (migration 029), share_session/unshare_session tools, resolveSessionIr, GET /live/<id>/glb. Every /live route 404s unless explicitly shared.
3a929019 A2 — the viewer app: a self-contained Vite single-file page (Three.js + supabase-js) served at GET /live/<id>. Subscribes to Realtime session:<id>, replays /events, folds the stream (kernel→refetch GLB, overlay→pins, control→banner), presence, click-to-pin. v1 point-based anchoring.
8aa929a8 review fixes (below).

Design: private by default, explicit + revocable share

  • A session is not viewable just because VCAD_LIVE_WINDOW is on — it needs a live_shares row written by share_session, which returns the link with an explicit PUBLIC-link warning. unshare_session revokes it.
  • The viewer gets only the publishable anon key, never the service-role key.
  • v1 sharing exposure decision (per product call): "link = anyone with it can view," but private until shared and clearly stated at share time.

Security review — 5 findings, all fixed before this PR

An adversarial review (security · correctness · integration) caught real issues:

  • (major) Realtime broadcast bypassed the share gate. Migration 028's trigger fanned every event to a public topic unconditionally — never-shared sessions streamed, unshare didn't stop new events, and it shipped full tool args. Migration 030 gates the broadcast on an active live_shares row and strips args from the fan-out (viewers get tool/type/changed, not the construction source; the durable row keeps full args for fold/replay).
  • (major) Geometry spoofing in resolveSessionIr. local_id is client-supplied (unique only per user); the documents fallback had no user filter, so anyone could sync local_id='mcp:<victimId>' and spoof a shared session's geometry. Now scoped to the sharer's user_id (via live_shares.shared_by); no owner → no fallback.
  • (minor) rate-limit now covers the gate query + HTML/config routes (one early check).
  • (minor) declared @supabase/supabase-js (was phantom-hoisted).
  • honest copy: unshare states the link 404s and new live updates stop, but an already-connected socket isn't force-closed — a private-channel + realtime.messages RLS follow-up.

Verification

  • 295 mcp tests pass (share store + getShare, resolveSessionIr anti-spoof + owner-scoping, share tools, live-route gate + glb routing). Typecheck + full build clean.
  • entry.ts (the Vercel function) bundles with esbuild, inlining the live HTML.
  • Browser smoke test: the viewer renders the LIVE badge, Three.js scene, and chrome with zero console errors.
  • supabase db push --dry-run recognizes migrations 029 + 030.

After merge

  • Migrations 029 + 030 deploy automatically now that SUPABASE_DB_PASSWORD is set.
  • VCAD_LIVE_WINDOW=1 is already on vcad-mcp; the git deploy serves /live.
  • I'll verify end-to-end against prod: share_session → open /live/<id> → watch a mutation render + a pin round-trip.

Deliberate follow-ups (not in this PR)

  • Face-fingerprint anchoring (normal+offset+centroid, refold re-resolve) — v1 ships point-based anchors.
  • Private Realtime channel + realtime.messages RLS — to force-close a viewer's socket on unshare (currently new events just stop).
  • Extract a shared scene.ts between the inline viewer and the live app.

🤖 Generated with Claude Code

ecto and others added 3 commits June 23, 2026 07:06
…ault)

Track A foundation for the "watch live" window. Sessions are PRIVATE by
default; a live link works only after an explicit, revocable share.

- migration 029 live_shares (session_id PK, service-role only) — the gate.
- session-store: ShareStore (isShared/share/unshare) + createShareStore;
  resolveSessionIr(id) — service-role resolve by session id alone (mcp_sessions,
  then documents by mcp:<id> with NO user filter) for the hostless viewer.
- tools/live-share.ts: share_session (returns the link + an explicit PUBLIC-link
  warning) / unshare_session (revoke). Registered in server.ts.
- live-route.ts: every /live route now 404s unless the session is shared
  (private-by-default gate), even with VCAD_LIVE_WINDOW on. New GET
  /live/<id>/glb: resolveSessionIr → generateGlbPreview → model/gltf-binary, so
  a link-holder with no MCP host and no login can fetch geometry. Engine passed
  in from http.ts + entry.ts.
- tests: share store, resolveSessionIr (mcp_sessions + documents fallback),
  share tools (link/warning/gate), live-route gate (private→404, shared→serve)
  + glb routing. Full mcp suite green (295).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Track A2: the "watch live" browser viewer, served at GET /live/<id> for a
shared session. Self-contained Vite single-file app (Three.js + supabase-js
inlined), mirroring the inline viewer's build pattern.

- live-app/: parses the session id from the path, subscribes to Supabase
  Realtime topic session:<id>, replays GET /events, folds the stream
  (kernel+changed → debounced GLB refetch via GET /live/<id>/glb; overlay →
  pin markers; control → banner), and shows presence on a separate ephemeral
  channel. Read-only for geometry; click-to-pin POSTs to /annotate and rides
  the broadcast back. v1 anchoring = a spatial point in kernel coords under the
  Z-up modelGroup (face-fingerprint anchoring is the documented fast-follow).
- live-route.ts: serves the viewer HTML (GET /live/<id>) and the realtime
  config (GET /live/<id>/config — publishable anon key only, never service
  role); both share-gated.
- build: vite.live.config.ts + scripts/wrap-live.mjs → live-html.generated.ts,
  chained into build/typecheck; dist-live + generated file gitignored.
- launch.json: mcp-live dev server (parallel to mcp-viewer).

Verified: live-app bundles (49 modules), typecheck clean, 295 mcp tests green,
and a browser smoke test renders the scene + chrome with zero console errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eal)

Adversarial security review of Track A surfaced five confirmed issues; all fixed.

- BLOCKER-ADJACENT (major, x2 lenses): the Realtime broadcast bypassed the share
  gate — migration 028's trigger fanned EVERY session_events insert to a public
  topic unconditionally, so never-shared sessions streamed and unshare didn't
  stop new events, and it shipped full tool args. migration 030 gates the
  broadcast on an active live_shares row (never-shared → silent; unshare → no new
  events) and strips `args` from the fan-out (viewers get tool/type/changed, not
  the construction source). The durable row keeps full args for fold/replay.
- MAJOR: resolveSessionIr's documents fallback had no user filter, and local_id
  is client-supplied (unique only per user) — any user could sync a doc with
  local_id 'mcp:<victimId>' and spoof a shared session's geometry. Now scoped to
  the sharer: ShareStore.getShare returns shared_by, and the documents query
  filters user_id=eq.<sharer>; with no owner it won't fall back at all.
- MINOR: the share gate query + HTML/config routes weren't rate-limited — moved
  one rate-limit check to cover every /live route up front.
- MINOR: declared @supabase/supabase-js as a devDependency (was phantom-hoisted).
- Honest copy: unshare_session now states the link 404s and new live updates
  stop, but an already-connected socket isn't force-closed (a private-channel +
  realtime.messages RLS follow-up).
- changelog: live review window feat entry.

Full mcp suite green (295); typecheck + build clean; migrations 029/030 recognized
by db push --dry-run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

4 Skipped Deployments
Project Deployment Actions Updated (UTC)
mecheval Ignored Ignored Jun 23, 2026 11:38am
vcad Ignored Ignored Jun 23, 2026 11:38am
vcad-docs Ignored Ignored Jun 23, 2026 11:38am
vcad-mcp Ignored Ignored Jun 23, 2026 11:38am

Request Review

@ecto
ecto merged commit 8fc8e47 into main Jun 23, 2026
12 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