Repository navigation
feat(mcp): live review window — watch a shared session build, drop pins - #312
Merged
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
The flagship of Track A: a "watch Claude build, live" review window. The backbone (event spine +
/liveroutes) 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
8e767db0live_shares(migration 029),share_session/unshare_sessiontools,resolveSessionIr,GET /live/<id>/glb. Every/liveroute 404s unless explicitly shared.3a929019GET /live/<id>. Subscribes to Realtimesession:<id>, replays/events, folds the stream (kernel→refetch GLB, overlay→pins, control→banner), presence, click-to-pin. v1 point-based anchoring.8aa929a8Design: private by default, explicit + revocable share
VCAD_LIVE_WINDOWis on — it needs alive_sharesrow written byshare_session, which returns the link with an explicit PUBLIC-link warning.unshare_sessionrevokes it.Security review — 5 findings, all fixed before this PR
An adversarial review (security · correctness · integration) caught real issues:
unsharedidn't stop new events, and it shipped full tool args. Migration 030 gates the broadcast on an activelive_sharesrow and stripsargsfrom the fan-out (viewers get tool/type/changed, not the construction source; the durable row keeps full args for fold/replay).resolveSessionIr.local_idis client-supplied (unique only per user); the documents fallback had no user filter, so anyone could synclocal_id='mcp:<victimId>'and spoof a shared session's geometry. Now scoped to the sharer'suser_id(vialive_shares.shared_by); no owner → no fallback.@supabase/supabase-js(was phantom-hoisted).unsharestates the link 404s and new live updates stop, but an already-connected socket isn't force-closed — a private-channel +realtime.messagesRLS follow-up.Verification
getShare,resolveSessionIranti-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.supabase db push --dry-runrecognizes migrations 029 + 030.After merge
SUPABASE_DB_PASSWORDis set.VCAD_LIVE_WINDOW=1is already onvcad-mcp; the git deploy serves/live.share_session→ open/live/<id>→ watch a mutation render + a pin round-trip.Deliberate follow-ups (not in this PR)
realtime.messagesRLS — to force-close a viewer's socket on unshare (currently new events just stop).scene.tsbetween the inline viewer and the live app.🤖 Generated with Claude Code