Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions docs-ai/046-mobile-remote-control/000-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# 046 — Mobile Remote Control: Plan

| | |
| --- | --- |
| **Status** | Implemented |
| **Anchor date** | 2026-07-13 |
| **Primary PRs** | #583 |
| **Related** | #196, [013-prowl-cli](../013-prowl-cli/000-plan.md), `docs/components/cli.md` |

## Background

Issue #196 asks for mobile access to a running Prowl instance. The existing `prowl`
control plane is intentionally local-only: `supacode/CLIService/CLISocketServer.swift`
uses an owner-only Unix socket and verifies the peer UID. Its generic command router
also includes terminal input and destructive actions, and its responses contain local
paths and agent-session metadata. It must not be placed behind a network listener.

The issue does not define a mobile client, transport, or remote write capability. The
owner identified a proof of concept as the appropriate first increment.

## Goals

- Add an opt-in, loopback-only read-only bridge for a separately authenticated private
tunnel or overlay.
- Require a high-entropy bearer credential stored outside `~/.prowl/settings.json`.
- Return a path-free agent projection and bounded viewport text through opaque IDs.
- Let Settings start and stop the listener immediately, with documented security bounds.

### Non-goals

- A native iOS client, Prowl-managed relay, public listener, TLS termination, push
streaming, or automatic tunnel configuration.
- Re-exporting the CLI protocol or exposing `send`, `key`, `focus`, `open`, `tab`, or
`pane` actions.
- Scrollback access, unbounded output, device-specific pairing, or per-device auditing.

## Design / Approach

1. Add a dedicated HTTP request router under `supacode/CLIService/`. It accepts only
authenticated `GET` requests for an agent summary and limited current viewport text;
it does not accept a `CommandEnvelope`.
2. Build remote DTOs directly from live app and terminal state in `supacode/App/supacodeApp.swift`.
They omit paths, CWDs, transcript paths, and raw terminal identifiers. Reads use a
short-lived opaque mapping plus line and UTF-8 byte caps.
3. Bind a small listener to `127.0.0.1` only. A Keychain-backed random bearer token is
never written to the global settings model or emitted through `SupaLogger`.
4. Add a public `remoteControlEnabled` setting through the global model, Settings
reducer, and Advanced settings view. An app-owned service coordinates lifecycle
changes without subscribing to the single-subscriber terminal event stream.
5. Cover authentication, allowlisting, redaction, output limits, settings persistence,
and start/stop behavior with tests, then document safe deployment.

## Alternatives & decisions

| Decision | Rejected alternative | Rationale |
| --- | --- | --- |
| Separate read-only protocol and DTOs | Forward the CLI socket or command router | The local trust model and payloads are unsafe at a network boundary. |
| Loopback listener plus user-managed private TLS tunnel | Bind LAN/public TCP directly | Prowl owns neither a relay nor certificate/identity infrastructure. |
| Keychain-backed rotating token | Persist it in `GlobalSettings` | Settings may be symlinked or copied into dotfiles and are not a secret store. |
| Request/response polling | Subscribe to terminal events | The terminal event stream currently permits one subscriber. |

## Amendments

- Updated 2026-07-31: Recorded TCP review hardening and corrected stale commit references — see
[002-review-follow-up.md](002-review-follow-up.md).
32 changes: 32 additions & 0 deletions docs-ai/046-mobile-remote-control/001-action.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# 046 — Mobile Remote Control: Action Log

## Timeline
| Date | Change | Ref |
| --- | --- | --- |
| 2026-07-13 | Added the read-only loopback bridge, Keychain credential store, opt-in Settings lifecycle, and regression tests. | `3a03bcf2` |
| 2026-07-13 | Added the agent-facing deployment and safety manual, then updated the docs sync baseline. | `c9a7493f` |
| 2026-07-28 | Hardened the TCP bridge after review: concurrent client handling with a connection deadline, active-socket shutdown on stop, `SIGPIPE` suppression, and TCP integration tests. | `155cb24f` |
| 2026-07-31 | Capped active connections, closed the accept/register shutdown race, and added boundary tests. | `ba1de11b` |

## Outcome & current state (as of 2026-07-31)

- `supacode/CLIService/RemoteControlRouter.swift` accepts authenticated `GET` requests
for active-agent summaries and bounded viewport reads only. It uses opaque IDs and
returns no local paths, CWDs, or agent-session files.
- `supacode/CLIService/RemoteControlServer.swift` binds only `127.0.0.1:39466`;
`RemoteControlAccessTokenStore.swift` holds a 32-byte rotating bearer token in the
macOS Keychain.
- `supacode/App/supacodeApp.swift`, `RemoteControlClient.swift`, and the Settings
feature start and stop the bridge immediately through `remoteControlEnabled`.
- `docs/components/remote-control.md` documents the private-tunnel requirement,
endpoint limits, token rotation, and explicit write-operation exclusions.

## Deviations from plan

The proof of concept limits reads to the current viewport rather than scrollback, a
stricter safety boundary than planned.

## Open questions

The first increment has no native mobile client, managed relay, device-specific pairing,
or push transport. Those require a separately specified security and delivery model.
35 changes: 35 additions & 0 deletions docs-ai/046-mobile-remote-control/002-review-follow-up.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# 046.002 — Review Follow-up

## Context

Review of #583 identified TCP lifecycle risks in the initial bridge. Commit `155cb24f`
made client I/O concurrent and time-bounded, shut active sockets down during stop, and
suppressed `SIGPIPE`. A later audit confirmed those mechanisms and found one remaining
resource-bound issue: the concurrent client queue admitted an unlimited number of
blocking handlers.

The same audit found stale references in the original action log and docs sync metadata.

## Change

- `RemoteControlConnectionRegistry` now admits at most 16 active sockets. Excess
connections are closed before a client worker is dispatched.
- `RemoteControlServer.stop()` now quiesces the accept queue before shutting down the
registered clients, so an accepted socket cannot register after the shutdown sweep.
- Tests cover rejection at the connection limit, capacity reuse after close, and the
stalled peer being closed before `stop()` returns.
- `001-action.md` now names the documentation, TCP hardening, and concurrency-cap
commits directly.
- The agent-facing remote-control manual was rechecked and required no behavioral edits.

## Refs

- PR #583
- `155cb24f`
- `ba1de11b`

## Current state

The bridge remains loopback-only, authenticated, and read-only. Blocking client work is
bounded by both the 16-connection admission limit and the existing per-connection I/O
timeouts and deadline.
1 change: 1 addition & 0 deletions docs-ai/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,4 @@ behavior (`docs/` is the agent-facing manual for that).
| 043 | [canvas-tile-layout](043-canvas-tile-layout/000-plan.md) | 2026-06-24 | Tile layout + default-layout setting |
| 044 | [foundation-model-branch-names](044-foundation-model-branch-names/000-plan.md) | 2026-06-27 | On-device FM branch-name suggestions |
| 045 | [native-agent-session-detection](045-native-agent-session-detection/000-plan.md) | 2026-07-12 | Native agent session identity (successor to 030's heuristics) |
| 046 | [mobile-remote-control](046-mobile-remote-control/000-plan.md) | 2026-07-13 | Opt-in read-only mobile remote-control bridge PoC |
6 changes: 3 additions & 3 deletions docs/.sync-meta.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"_comment": "Metadata for the docs/ manual. The last_synced_commit is the commit at which docs/ was last verified against the implementation; the sync-docs skill (.claude/skills/sync-docs/SKILL.md) diffs HEAD against it and updates this file. Committed to git on purpose so the baseline persists across sessions and machines. A dotfile + .json so a future docs website does not render it as a page.",
"last_synced_commit": "168d8e9c8e61655f57dcb8411092b0af9263e7e0",
"last_synced_date": "2026-07-10",
"note": "Release prep. All user-facing changes in range were doc-synced in their own PRs (#545 sound picker, #546 mute viewed surface, #542 editor additions, #543 TERM_PROGRAM, #541 gh hardening); #544 symlink-preserving writes and PR tri-state fixes need no doc changes."
"last_synced_commit": "ba1de11bb61c7a9a9f93d19bb4424d22261991f1",
"last_synced_date": "2026-07-31",
"note": "Remote-control manual rechecked after TCP client concurrency and shutdown hardening; no behavioral edits required."
}
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ its keyboard shortcuts, detailed behavior, settings, and gotchas.
| [`components/settings.md`](components/settings.md) | The Settings window (`⌘,`): every tab and what it controls. |
| [`components/updates.md`](components/updates.md) | Sparkle auto-updates: auto-check, `⌘⇧U`. |
| [`components/cli.md`](components/cli.md) | The `prowl` CLI — let an agent inspect and drive panes (`list`, `read`, `send`, `key`, `focus`, `tab`, `pane`, `open`). |
| [`components/remote-control.md`](components/remote-control.md) | Experimental read-only mobile bridge: loopback-only deployment, private tunnel boundary, token rotation, agent summaries, and bounded viewport reads. |

## Reference (exact lookups)

Expand All @@ -81,6 +82,7 @@ its keyboard shortcuts, detailed behavior, settings, and gotchas.
| Review what an agent changed | [`components/diff-view.md`](components/diff-view.md) |
| Open / merge / re-run CI on a pull request | [`components/github-pull-requests.md`](components/github-pull-requests.md) |
| Drive a pane from a script or another agent | [`components/cli.md`](components/cli.md) |
| Check active agents from a trusted phone | [`components/remote-control.md`](components/remote-control.md) |
| Look up a keyboard shortcut | [`reference/keyboard-shortcuts.md`](reference/keyboard-shortcuts.md) |
| Change app behavior / a setting | [`components/settings.md`](components/settings.md), [`reference/settings-fields.md`](reference/settings-fields.md) |

Expand Down
6 changes: 5 additions & 1 deletion docs/components/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

**Keywords:** prowl cli, command line, prowl list, prowl agents, prowl read, prowl send, prowl key, prowl focus, prowl tab, prowl pane, prowl open, pane id, automation, json, capture, socket

**Related:** [terminal](terminal.md) · [concepts](../concepts.md) · [active-agents](active-agents.md) · [agent-detection](agent-detection.md) · the bundled **`prowl-cli` skill** (`skills/prowl-cli/SKILL.md`)
**Related:** [terminal](terminal.md) · [concepts](../concepts.md) · [active-agents](active-agents.md) · [agent-detection](agent-detection.md) · [remote-control](remote-control.md) · the bundled **`prowl-cli` skill** (`skills/prowl-cli/SKILL.md`)

> This is the reference for the `prowl` binary. For an opinionated, safety-first
> *workflow* guide (recipes, pitfalls, quoting), the repository also ships the
Expand All @@ -20,6 +20,10 @@ agent, run something in another tab and grab the output, focus a worktree, open
project, or close a scratch tab. It is **not** for ordinary editing/building
inside a repo, and not for how-to questions about Prowl's settings.

`prowl` remains same-Mac, same-user IPC. It is not a remote/mobile endpoint; the
separate experimental [read-only mobile bridge](remote-control.md) has a narrower
API, a bearer token, and must be exposed only through a private authenticated tunnel.

## Install

From the app: **Settings → Advanced → Install Command Line Tool**, or Command
Expand Down
47 changes: 47 additions & 0 deletions docs/components/remote-control.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Remote Control (Experimental)

> A deliberately limited, read-only bridge for checking active Prowl agents from a
> companion mobile client. It is not a remote terminal or a replacement for the
> local `prowl` CLI.

**Keywords:** remote control, mobile, bridge, bearer token, loopback, private tunnel, agent status, viewport

**Related:** [active-agents](active-agents.md) · [settings](settings.md) · [cli](cli.md)

## Enable and pair

Open **Settings → Advanced → Remote Control (Experimental)** and enable the
read-only bridge. It listens only at `127.0.0.1:39466`; it is never reachable from
the LAN or public internet by itself.

Copy the access token from the same section into a trusted companion client. The
token is stored in the macOS Keychain, not in `~/.prowl/settings.json`. **Rotate and
Copy Access Token** immediately revokes clients using the old token.

To reach the bridge from a phone, provide your own authenticated private transport
that terminates TLS and forwards only to `127.0.0.1:39466`—for example, a private
overlay or an authenticated reverse tunnel. Prowl does not create or manage a tunnel.
Never forward `cli.sock` or expose the bridge directly on a LAN/public interface.

## Read-only API

Every request must send `Authorization: Bearer <access-token>`.

| Request | Result |
| --- | --- |
| `GET /v1/agents` | Active-agent summaries with opaque IDs, status, display/project/branch names, and no local paths or session files. |
| `GET /v1/agents/<opaque-id>/read?last=1…80` | The current terminal viewport for that active agent. Output is capped at 80 lines and 12 KiB UTF-8; `truncated` is `true` when either cap applies. |

The API accepts no writes: there is no endpoint for text input, keys, focus, opening,
closing, tab, or pane operations. It also does not expose scrollback or live streaming.

## Safety boundaries

- The bearer token is required even on loopback because a tunnel is a separate trust
boundary from Prowl's same-UID CLI socket.
- The bridge uses opaque IDs that are refreshed from the current active-agent set;
re-fetch `/v1/agents` after an agent disappears or Prowl restarts.
- Terminal text can still contain sensitive project data. Pair only a device you trust,
protect the private tunnel, and rotate the token if it may have been disclosed.
- Turning the setting off stops the listener immediately. It retains the Keychain token
so a trusted setup can be re-enabled; rotate it to revoke prior access.
2 changes: 1 addition & 1 deletion docs/components/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ window is a sidebar of tabs plus a detail pane.
| **Shortcuts** | Remap app keyboard shortcuts; view defaults; resolve conflicts. → [keyboard-shortcuts](../reference/keyboard-shortcuts.md) |
| **Worktree** | Worktree creation/deletion defaults: prompt on create, fetch before create, base directory, copy ignored/untracked files, delete-branch-on-delete, merged-worktree action, archived auto-delete period. |
| **Updates** | Auto-check toggle, "Check for Updates Now". → [updates](updates.md) |
| **Advanced** | Analytics, crash reports, restore terminal layout on launch (experimental) + clear saved layout, and the **Install Command Line Tool** (`prowl` CLI) action. |
| **Advanced** | Analytics, crash reports, restore terminal layout on launch (experimental) + clear saved layout, the **Install Command Line Tool** (`prowl` CLI) action, and the experimental [read-only mobile bridge](remote-control.md). |
| **GitHub** | Enable GitHub integration (uses the `gh` CLI). → [github-pull-requests](github-pull-requests.md) |
| **Repositories / Repo Settings** | Per-repository: setup/archive/run scripts, **Custom Commands**, default base ref & directory, copy-files overrides, open-with app, custom title, icon & color, PR merge strategy, line-diff & PR-state fetching. Reached from the sidebar context menu → "Repo Settings". → [custom-actions](custom-actions.md), [repositories-and-worktrees](repositories-and-worktrees.md) |

Expand Down
1 change: 1 addition & 0 deletions docs/reference/settings-fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ JSON is pretty-printed with sorted keys. Legacy `~/.supacode` is migrated to
| `copyUntrackedOnWorktreeCreate` | Bool | `false` | Copy untracked files into new worktrees. |
| `pullRequestMergeStrategy` | enum (`merge`/`squash`/`rebase`) | `merge` | Default PR merge strategy. |
| `restoreTerminalLayoutOnLaunch` | Bool | `false` | Restore tabs/splits on launch. |
| `remoteControlEnabled` | Bool | `false` | Start the experimental, loopback-only read-only mobile bridge. Its bearer token is held in the macOS Keychain and is never stored in this JSON file. |
| `terminalFontSize` | Float32? | `nil` | Remembered terminal font size. |
| `archivedAutoDeletePeriod` | enum? (days) | `nil` | Auto-delete archived worktrees after N days; `nil` = never. |
| `keybindingUserOverrides` | object | empty | User keyboard-shortcut remappings. |
Expand Down
Loading