Skip to content

feat(control): remote-control policy — gate high-risk actions from remote callers#114

Merged
oratis merged 1 commit into
mainfrom
feat/remote-control-gating
Jun 19, 2026
Merged

feat(control): remote-control policy — gate high-risk actions from remote callers#114
oratis merged 1 commit into
mainfrom
feat/remote-control-gating

Conversation

@oratis

@oratis oratis commented Jun 19, 2026

Copy link
Copy Markdown
Owner

Stacked on #113 (base = feat/ios-companion-and-pty-cli). Retarget to main once #113 merges.

What

Closes the security gap from #113's follow-ups: a phone holding a valid token could call every control endpoint, including adopting the user's real claude sessions. This adds a second authorization axis — a Mac-side policy that gates high-risk control actions from remote (non-loopback) callers. The local user (loopback) is never gated.

src/control/policy.tsControlPolicy:

  • remoteControl (default true) — a remote may control LISA's own agents (managed/pty: start/send/cancel/approve). Lower-risk, LISA-owned.
  • remoteAdoptExternal (default false) — a remote may adopt the user's external sessions (pty/start with resumeSessionId, i.e. claude --resume). Touches a real transcript → off until the Mac owner opts in.

Persisted to ~/.lisa/control-policy.json; tolerant of missing/corrupt files.

How

  • GET /api/control/policy (any authed caller) reports it; POST /api/control/policy sets it but only from localhost (like the API-key save).
  • A per-request denyRemote(need) helper (loopback ⇒ allow; else consult policy) writes the 403 and short-circuits. Applied to: managed start / send / cancel / approve; pty start (→ adoptExternal when resumeSessionId is present, else control), send, cancel, output, stream; and signal cancel (list stays open — it's telemetry).

Verification

  • npm run typecheck + npm run build clean; 733 tests / 732 pass / 1 skip / 0 fail (new: 5 policy unit tests).
  • Live-checked from a non-loopback LAN IP (--host 0.0.0.0 + token):
    • GET policy{remoteControl:true, remoteAdoptExternal:false}
    • adopt-external from remote → 403 (default deny)
    • POST policy from remote → 403; from loopback → 200
    • after remoteControl:false: managed/start from remote → 403
    • loopback calls never gated.

Follow-ups (unchanged from #113)

Next in queue: roster de-dup (resume-adopt vs claude-code observer) + gated /api/dispatch/status log tail; then the iOS-facing backend (pairing/devices/push) and PWA polish. The native SwiftUI app (ActivityKit/WidgetKit/APNs) will be scaffolded but can't be compiled/verified in this repo.

🤖 Generated with Claude Code

…mote callers

src/control/policy.ts: ControlPolicy { remoteControl (default true), remoteAdoptExternal (default false) }, persisted to ~/.lisa/control-policy.json, loopback-settable.

server: GET/POST /api/control/policy (POST is loopback-only); a denyRemote() gate on managed start/send/cancel/approve, pty start (+ resumeSessionId ⇒ adoptExternal), pty send/cancel/output/stream, and signal cancel. The Mac owner (loopback) is never gated.

Net effect: a phone (token) can see + control LISA's OWN agents by default, but cannot adopt the user's EXTERNAL sessions until the Mac owner opts in.

Verified: typecheck + build clean; 733 tests pass; live-checked the 403s from a non-loopback LAN IP.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@oratis oratis changed the base branch from feat/ios-companion-and-pty-cli to main June 19, 2026 05:07
@oratis oratis merged commit 206879a into main Jun 19, 2026
1 check passed
@oratis oratis deleted the feat/remote-control-gating branch June 19, 2026 05:09
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.

1 participant