feat(realtime,desktop): remote-control safety envelope (LC-186) #230

Merged
nrupard merged 1 commit from feat/lc-186-remote-control-safety into main 2026-05-27 16:31:06 +02:00
Owner

Final subtask of the TeamViewer-style remote-control story (LC-181): the safety envelope around a live control session. Builds on LC-183 (consent gate, #227), LC-184 (controller capture, #228), LC-185 (OS injection, #229).

Kill-switch (sharer side)

  • Banner + button. A persistent red banner (data-lc-control-banner) shows the whole time a peer holds control, with a "Stop control" button.
  • Global hotkey. CommandOrControl+Alt+F9 registered natively via tauri-plugin-global-shortcut - the reliable sever when the controller holds the input focus (clicking the button is hard while a remote cursor moves). The handler flips the injector off + releases held input directly (ControlState::kill, independent of the webview), then window.evals a lc:control-kill event so the page signals revoke and drops the banner.

Auto-revoke (call.js revokeAsSharer)

On call end (teardown), screen-share stop (endScreenShare), and data-channel drop (controlChannel.onclose/onerror): signal revoke (controller stops capturing + audit closes) and disarm locally. The design's "focus loss to a higher-integrity window" auto-revoke is intentionally not implemented - UIPI already blocks injection into elevated windows, so the exposure does not exist; instead the controller sees a standing UIPI hint (data-lc-control-uipi) so it is never silently no-op'd.

Audit

chat.db remote_control_sessions (migration 0052) via db::remote_control_audit. relay_control_signal opens a row on grant (controller = recipient, sharer = granter) and closes it on revoke; a WS-disconnect backstop (end_sessions_for_user) closes any session a hard drop left open. Only participant ids + timestamps are stored - never content.

Limits

RateLimitKind::RemoteControlRequest caps request signals at 10/min per requester (over-limit drops silently, same posture as the verified/block gate). The LC-183 verified-email + block-list gate still applies.

Decision (recorded in the design doc): no max-session-duration / periodic re-consent in v1. The instant kill-switch + auto-revoke + disconnect-close already bound exposure; periodic re-consent adds friction without a matching threat once an instant sever exists. Revisit if a concrete need appears.

Verification

  • just test + just test-saas green, including a new db_remote_control_audit integration test (start/end-by-room, idempotent re-grant, end-for-user closes both roles).
  • Desktop cargo check + clippy + fmt clean.
  • cargo check --target x86_64-pc-windows-gnu clean (global-shortcut + injector path).

Story status

LC-182 design, LC-183 gate, LC-184 controller, LC-185 injection, LC-186 safety - all landed. Remaining future work (separate, not blocking): Linux/macOS injection backends; manual end-to-end verification on a real Windows box (LC-180).

Final subtask of the TeamViewer-style remote-control story (LC-181): the safety envelope around a live control session. Builds on LC-183 (consent gate, #227), LC-184 (controller capture, #228), LC-185 (OS injection, #229). ## Kill-switch (sharer side) - **Banner + button.** A persistent red banner (`data-lc-control-banner`) shows the whole time a peer holds control, with a "Stop control" button. - **Global hotkey.** `CommandOrControl+Alt+F9` registered natively via `tauri-plugin-global-shortcut` - the reliable sever when the controller holds the input focus (clicking the button is hard while a remote cursor moves). The handler flips the injector off + releases held input **directly** (`ControlState::kill`, independent of the webview), then `window.eval`s a `lc:control-kill` event so the page signals `revoke` and drops the banner. ## Auto-revoke (`call.js` `revokeAsSharer`) On **call end** (teardown), **screen-share stop** (`endScreenShare`), and **data-channel drop** (`controlChannel.onclose/onerror`): signal `revoke` (controller stops capturing + audit closes) and disarm locally. The design's "focus loss to a higher-integrity window" auto-revoke is **intentionally not implemented** - UIPI already blocks injection into elevated windows, so the exposure does not exist; instead the controller sees a standing UIPI hint (`data-lc-control-uipi`) so it is never silently no-op'd. ## Audit `chat.db` `remote_control_sessions` (migration 0052) via `db::remote_control_audit`. `relay_control_signal` opens a row on `grant` (controller = recipient, sharer = granter) and closes it on `revoke`; a **WS-disconnect backstop** (`end_sessions_for_user`) closes any session a hard drop left open. Only participant ids + timestamps are stored - never content. ## Limits `RateLimitKind::RemoteControlRequest` caps `request` signals at **10/min per requester** (over-limit drops silently, same posture as the verified/block gate). The LC-183 verified-email + block-list gate still applies. **Decision (recorded in the design doc): no max-session-duration / periodic re-consent in v1.** The instant kill-switch + auto-revoke + disconnect-close already bound exposure; periodic re-consent adds friction without a matching threat once an instant sever exists. Revisit if a concrete need appears. ## Verification - `just test` + `just test-saas` green, including a new `db_remote_control_audit` integration test (start/end-by-room, idempotent re-grant, end-for-user closes both roles). - Desktop `cargo check` + `clippy` + `fmt` clean. - `cargo check --target x86_64-pc-windows-gnu` clean (global-shortcut + injector path). ## Story status LC-182 design, LC-183 gate, LC-184 controller, LC-185 injection, LC-186 safety - all landed. Remaining future work (separate, not blocking): Linux/macOS injection backends; manual end-to-end verification on a real Windows box (LC-180).
feat(realtime,desktop): remote-control safety envelope (LC-186)
Some checks failed
check-secrets / Nosey parker (push) Successful in 6s
check-secrets / TruffleHog (push) Successful in 6s
check-secrets / Kingfisher (push) Successful in 8s
check-secrets / TruffleHog (pull_request) Successful in 8s
check-secrets / Nosey parker (pull_request) Successful in 8s
check-secrets / Kingfisher (pull_request) Successful in 10s
Check / clippy + fmt + tests (pull_request) Failing after 36s
Create release / Create release from merged PR (pull_request) Has been skipped
381cd67def
Final subtask of the TeamViewer-style remote-control story (LC-181): the kill-switch, auto-revoke, audit trail, and abuse limits around a live control session. Builds on LC-183 (consent gate), LC-184 (controller capture), LC-185 (OS injection).

Kill-switch (sharer side): a persistent red banner shows the whole time a peer holds control, with a "Stop control" button. A global OS hotkey (CommandOrControl+Alt+F9, via tauri-plugin-global-shortcut) is the reliable sever when the controller holds the input focus - its native handler flips the injector off and releases held input directly (ControlState::kill, no webview dependency), then evals a lc:control-kill DOM event so the page signals revoke and drops the banner.

Auto-revoke (call.js revokeAsSharer): on call end, screen-share stop, and data-channel drop, the sharer signals revoke (controller stops capturing, audit row closes) and disarms locally. The "focus loss to a higher-integrity window" auto-revoke from the design is intentionally not implemented - UIPI already blocks injection into elevated windows, so the controller instead sees a standing UIPI hint that some windows can't be driven (not silently no-op'd).

Audit: chat.db remote_control_sessions (migration 0052) via db::remote_control_audit. relay_control_signal opens a row on grant (controller = recipient, sharer = granter) and closes it on revoke; a WS-disconnect backstop closes sessions a hard drop left open. Only participant ids + timestamps are stored, never content.

Limits: RateLimitKind::RemoteControlRequest caps request signals at 10/min per requester (over-limit drops silently); the LC-183 verified-email + block-list gate still applies. Decision recorded in the design doc: no max-session-duration / periodic re-consent in v1, since the instant kill-switch + auto-revoke + disconnect-close already bound exposure.

Verified: just test + just test-saas green (incl. a new db_remote_control_audit integration test), desktop check + clippy + fmt, and cargo check --target x86_64-pc-windows-gnu for the global-shortcut + injector path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
nrupard deleted branch feat/lc-186-remote-control-safety 2026-05-27 16:31:06 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!230
No description provided.