Skip to content

fix(remote): make the QR-code token view-only; lock down origins and add CSP - #268

Merged
johannesjo merged 5 commits into
mainfrom
claude/important-improvements-jiyttp
Sep 4, 2026
Merged

fix(remote): make the QR-code token view-only; lock down origins and add CSP#268
johannesjo merged 5 commits into
mainfrom
claude/important-improvements-jiyttp

Conversation

@johannesjo

Copy link
Copy Markdown
Owner

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

…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
@johannesjo
johannesjo merged commit 3d29839 into main Sep 4, 2026
2 checks passed
@johannesjo
johannesjo deleted the claude/important-improvements-jiyttp branch September 4, 2026 23:13
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.

2 participants