fix(remote): make the QR-code token view-only; lock down origins and add CSP - #268
Merged
Merged
Conversation
…add CSP The mobile token travels in the QR-code URL over plain HTTP, yet it could type into any agent terminal, which is code execution on the desktop for anyone who captured that URL. PRIVACY.md already promised the token was read-only; the code now matches: - WebSocket `input` requires the paired token (PIN entered on the phone) or the coordinator token. The mobile token may only subscribe. Resize and kill stay coordinator-only. Paired tokens can now authenticate the socket; the phone reconnects with its paired token after pairing and falls back to the QR-code token when the paired one goes stale. - Notes PUT requires the paired token; GET stays readable. - Browser `Origin` must match the request `Host` on the WebSocket upgrade and on every API route. Non-browser clients (no Origin) are unaffected. WebSockets are exempt from the same-origin policy, so this was the only missing gate against a cross-site page replaying the token. - Content-Security-Policy on the mobile SPA (header, socket pinned to the request host) and on the packaged desktop renderer (build-time meta tag; scripts limited to the bundle plus wasm for shiki). Verified the built renderer loads with zero policy violations. - Paired-token set is capped so pairing cannot grow it unbounded. The phone UI detours to the pairing screen when the user tries to type or save notes without a paired token, and returns to the agent afterwards. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016V9zaFQrv8aoUY2CuGuRJH
…ush failures - Autosave was a pure trailing debounce: every change reset the 1s timer, so continuous typing (notes panel) postponed the write indefinitely and a crash lost the whole session. Add a 5s maximum wait per burst and flush pending work when the effect's owner is disposed. - A failed state write only reached the console; tasks, projects and settings silently stopped persisting. Surface it as a longer-lived toast, rate-limited to once a minute while the cause (full disk, permissions) persists. - mergeTask/pushTask returned silently on precondition failures (task gone, being closed, direct mode, project folder missing), which read as "Merge did nothing". They now throw with a reason, and the merge and push dialogs show the message rather than a raw stringified error. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016V9zaFQrv8aoUY2CuGuRJH
Every task's terminals stay mounted, so each pane kept a live WebGL context for the life of the app; with many tasks that blew past Chromium's active-context cap (raised to 64 in main.ts) and contexts were evicted in rotation, the churn the webglContextLoss policy brakes on. TerminalView now detaches the WebGL addon a second after a pane leaves the screen (focus mode inactive task, tiling task scrolled fully out of view, unselected tab within a task) and reattaches on the visible edge. Hidden panes render through xterm's DOM renderer meanwhile; panes scrolled off screen are already paused by xterm's own IntersectionObserver. The glyph atlas is ref-counted per terminal inside xterm, so disposing one pane's addon leaves the others' intact. A fresh attach repaints the pane in full, which also covers the macOS corrupt-atlas case (issue #121) that the old visible-edge redraw handled; that redraw is kept only for a pane that returns before its detach delay elapses. TaskAITerminal passes tab visibility down instead of repainting itself. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016V9zaFQrv8aoUY2CuGuRJH
Coverage was collected but never gated. Add thresholds a few points below the measured totals (lines 58, statements 56, functions 44, branches 52) so a change that drops coverage noticeably fails CI, and run the unit suite with coverage via a new test:ci script. The coordinator's prompt-delivery integration suite against a real pty was env-gated and therefore never ran in CI; it passes in ~20s on a Linux runner, so run it as its own step. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016V9zaFQrv8aoUY2CuGuRJH
… detach Adversarial and maintainer reviews of the previous four commits found: - Arena battle panes pass competitor ids as taskId, so in focus mode they would have dropped to the DOM renderer while fully visible. TerminalView gains a `standalone` flag that skips task-level visibility; BattleScreen sets it. - Disposing the WebGL addon leaves the GL context in Chromium's active set until the canvas is garbage collected. Detach now loses the context explicitly, and the context-loss handler ignores an addon that is no longer current so that synthetic loss is not counted as a GPU event. - The Origin-check docstring overclaimed DNS-rebinding protection; the bearer token in per-origin localStorage is what defeats that. Reworded, and the dead hybi-08 / array-header branches removed. - The auth-timeout close reused 4001, which the phone treats as "token is stale" and discards; a slow network would have forced a re-pair. It is 4002 now. - The pairing detour unmounted the agent view and lost the typed prompt or notes draft. Drafts now live in module state keyed by agent and are restored on return. Typing also checks that the open socket itself is paired (not just localStorage) and reconnects if not. - setupAutosave runs outside any owner (App calls it after awaits), so its function-scope onCleanup never registered and only logged a dev warning. Removed; the max-wait is the real fix. Test tightened to assert the forced save lands exactly at the max wait. - Coverage floors were within 0.2 points of the measured totals; lowered to ~3 points under so ordinary drift does not fail CI. - Save-failure test uses a fake clock and asserts the reminder returns after the rate-limit interval. - Smaller: CSP header only on HTML responses; renderer img-src allows plain-http markdown images; paired-token cap comment states it bounds future auths only; pending reconnect timer cleared on the paired-token fallback; rebase error uses errMessage like the merge path; pairing copy matches the desktop button; PRIVACY.md lists the paired token as a second stored credential; guard message names the Relink button. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016V9zaFQrv8aoUY2CuGuRJH
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.
The mobile token travels in the QR-code URL over plain HTTP, yet it could
type into any agent terminal, which is code execution on the desktop for
anyone who captured that URL. PRIVACY.md already promised the token was
read-only; the code now matches:
inputrequires the paired token (PIN entered on the phone)or the coordinator token. The mobile token may only subscribe. Resize
and kill stay coordinator-only. Paired tokens can now authenticate the
socket; the phone reconnects with its paired token after pairing and
falls back to the QR-code token when the paired one goes stale.
Originmust match the requestHoston the WebSocket upgradeand on every API route. Non-browser clients (no Origin) are unaffected.
WebSockets are exempt from the same-origin policy, so this was the only
missing gate against a cross-site page replaying the token.
request host) and on the packaged desktop renderer (build-time meta tag;
scripts limited to the bundle plus wasm for shiki). Verified the built
renderer loads with zero policy violations.
The phone UI detours to the pairing screen when the user tries to type or
save notes without a paired token, and returns to the agent afterwards.
Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_016V9zaFQrv8aoUY2CuGuRJH