fix(mount): scope the websocket dial to the mount root - #447
Conversation
A path-scoped mount could never use the websocket. websocketURL dialed
/v1/workspaces/{id}/fs/ws with only token+cursor, and the server branches on
the path query: with none, it authorizes against a workspace-WIDE fs:read
capability. Sandbox mounts carry a token scoped to one subtree, so the
handshake returned 403 and the daemon fell back to polling:
websocket unavailable; using polling sync: failed to WebSocket dial:
expected handshake response status code 101 but got 403
Observed on a real cloud sandbox mounting /workflows/runs/<id>, where it
silently cost every scoped mount its real-time propagation.
Send two filters for a scoped mount: the bare root first, then root + "/**".
The server authorizes against the first (which is what the token covers) and
ORs them when matching events. The glob is not optional -- webSocketPathMatches
compares segment counts rather than prefixes, so "/a" does not match
"/a/b.md"; sending only the root authorizes and then delivers nothing, which
is a silent failure worse than the 403. An earlier version of this patch did
exactly that and broke TestAssessPropagationLatencyWebSocket.
Root mounts keep the historical unscoped dial unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughWebSocket URL construction now receives the mount remote root. Scoped mounts request the exact root and recursive subtree paths. Root mounts remain unscoped. Tests cover path filters, protocol conversion, and reconnect cursors. ChangesWebSocket mount scoping
Estimated code review effort: 2 (Simple) | ~10 minutes Merge Risk: ⚪ Minimal · up to Scoped mounts now receive real-time websocket updates for their mounted subtree while root mounts retain existing behavior; no actionable merge-blocking risk remains after normal checks and review. Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Relayfile Eval ReviewRun: Passed: 4 | Needs human: 0 | Reviewable: 0 | Missing output: 0 | Failed: 0 | Skipped: 0 Human Review CasesNo reviewable human-review cases captured Relayfile output. |
The bug
A path-scoped mount can never use the websocket.
websocketURLdials/v1/workspaces/{id}/fs/wswith onlytokenandcursor— nopath. The server branches on exactly that param:With no
path, the server requires a workspace-widefs:readcapability. Sandbox mounts carry a token deliberately scoped to one subtree, so the handshake 403s and the daemon falls back to polling:Observed on a real cloud sandbox mounting
/workflows/runs/<id>againstdev.file.agentrelay.com. Every scoped mount silently loses real-time propagation and degrades to the poll interval — measured elsewhere as roughly 170ms → 5s+.Broad user tokens are unaffected, which is why this went unnoticed: a personal mount with workspace-wide scopes connects fine.
The fix
Scoped mounts now send two
pathfilters:paths[0]— the bare root, which is exactly what the mount token covers.Root mounts (
/or empty) keep the historical unscoped dial, unchanged.Both filters are required. My first attempt sent only the bare root and broke
TestAssessPropagationLatencyWebSocket— the mirror never converged.webSocketPathMatchescompares segment counts, not prefixes:So root-only would authorize successfully and then match nothing — a silent failure, strictly worse than the 403 it replaced. That existing latency test is what caught it.
Verification
TestWebSocketURLScopesToMountRoot— asserts the scoped dial sends root-then-glob in that order, and that root mounts still send nopathgo test ./internal/mountsync/— full suite green (118s), includingTestAssessPropagationLatencyWebSocket/PollOnlygo build ./internal/...cleanNote for reviewers
Not yet verified against a live scoped mount — that needs this built into a
relayfile-mountrelease and pulled into a cloud sandbox snapshot. The 403 is reproducible today on any cloud workflow sandbox, so it should be straightforward to confirm after release.🤖 Generated with Claude Code