docs: rewrite README and add architecture docs

This commit is contained in:
Yejun Su 2026-03-20 20:46:36 +08:00
parent 63a879046b
commit 54267f1842
No known key found for this signature in database
GPG Key ID: AD03A563F321CA44
2 changed files with 175 additions and 2 deletions

View File

@ -1,5 +1,9 @@
# pi-remote-control # pi-remote-control
## Disclaimer
This project is for personal use and research only. It is provided as-is, and the author accepts no liability for any damage, loss, misuse, or operational consequences that result from installing or using it. Do not use it for safety-critical, multi-user, or untrusted-network deployments.
## Install ## Install
```bash ```bash
@ -11,7 +15,34 @@ pi install https://github.com/goofansu/pi-remote-control
Run `/remote-control` to open the menu: Run `/remote-control` to open the menu:
- **Turn on / Turn off** — start or stop the server - **Turn on / Turn off** — start or stop the server
- **Configure URL** — set the public base URL your proxy/tunnel exposes (saved to `~/.pi/agent/remote-control.json`) - **Configure URL** — set the base URL exposed by your local tunnel or proxy, saved to `~/.pi/agent/remote-control.json`
- **Status** — show the QR code and connection URL (only when server is running) - **Status** — show the QR code and connection URL (only when server is running)
On first use, you'll be prompted to configure the URL before the server starts. On first use, configure the URL before the server can start.
To start the server automatically on launch:
```bash
pi --remote-control
```
## Use case
The remote-control server binds to `127.0.0.1` on the host running `pi` and is reached through a local tunnel or proxy — in this case [Surge Ponte](https://kb.nssurge.com/surge-knowledge-base/guidelines/ponte), which provides an end-to-end encrypted device-to-device tunnel without exposing the server to the LAN.
The setup is:
1. Install this extension on the Mac that runs `pi`.
2. Enable Surge Ponte on that Mac and give it a device name such as `pi`.
3. On the same Mac, open `pi` and run the `/remote-control` command.
4. Choose `Configure URL` and set the base URL to your Surge Ponte hostname, for example `http://pi.sgponte`.
5. Choose `Turn on`.
6. Open `Status` to get the QR code and one-time connection URL.
7. On another device on the same Surge Ponte network, open that URL in a browser.
In this setup, the browser URL is `http://pi.sgponte:<dynamic-port>`, but the traffic is still routed through Surge Ponte's tunnel between your devices.
## Security notes
- The server only listens on localhost. Remote access depends on whatever local tunnel or proxy you configure.
- If you use a reverse proxy instead of Surge Ponte, configure it to terminate TLS at a fixed `https://` endpoint and forward to the server's dynamic backend port. Do not expose the dynamic port directly over a public network, as the server does not support HTTPS and any token or session cookie would be transmitted in cleartext.

142
docs/ARCHITECTURE.md Normal file
View File

@ -0,0 +1,142 @@
# Architecture
## Overview
`pi-remote-control` is a pi extension that exposes a running pi session over
HTTP/WebSocket. The machine running pi is the **host**; any browser that
connects is a **guest**.
The server binds to `127.0.0.1` only (never the LAN). A local proxy or tunnel
(e.g. Surge Ponte) forwards external traffic to it. The guest interacts
through a self-contained single-page app served inline — no CDN, no external
assets.
```mermaid
graph LR
subgraph Host["Host machine"]
PI["pi process"]
EXT["remote-control extension"]
SRV["HTTP + WS server — 127.0.0.1:port"]
PI -->|events| EXT
EXT -->|broadcast| SRV
SRV -->|sendUserMessage / abort| PI
end
PROXY["Surge Ponte"]
SRV <-->|localhost| PROXY
subgraph Guest["Guest browser"]
UI["Single-page app"]
end
PROXY <-->|HTTPS / WSS| UI
```
## Files
| File | Purpose |
|------|---------|
| `extensions/pi-remote-control/index.ts` | Extension entry point — registers flag, command, and event bridge |
| `extensions/pi-remote-control/server.ts` | HTTP + WebSocket server, auth enforcement, client management |
| `extensions/pi-remote-control/messages.ts` | Wire protocol: serialize session entries → `RenderMsg`, build `sync` payloads |
| `extensions/pi-remote-control/html.ts` | Inline single-page web UI (self-contained, no external deps) |
| `extensions/pi-remote-control/auth.ts` | One-time token generation/validation, session cookie helpers |
| `extensions/pi-remote-control/config.ts` | Read/write `remote-control.json`, public URL normalization and config UI |
## Authentication Flow
First-visit authentication exchanges the one-time token for a 24-hour session
cookie, keeping the token out of browser history and subsequent requests.
Note that the initial `GET /?token=TOKEN` may still be recorded by any proxy
or tunnel sitting in front of the server before the 302 redirect issues the cookie.
```mermaid
sequenceDiagram
actor Guest
participant Server
Note over Guest: Has URL with ?token=TOKEN
Guest->>Server: GET /?token=TOKEN
Server->>Server: validateToken() - timing-safe compare
Server-->>Guest: 302 to / + Set-Cookie pi_rc_session=ID HttpOnly
Guest->>Server: GET / (cookie only)
Server-->>Guest: 200 HTML with CSP nonce
Guest->>Server: GET /ws (cookie in header)
Server->>Server: isAuthenticated() - checks session map
Server-->>Guest: 101 Switching Protocols
Note over Server,Guest: WebSocket open, full sync sent immediately
```
**Security headers on every HTML response:**
- `Content-Security-Policy` with a per-request nonce (no `unsafe-inline`)
- `X-Frame-Options: DENY`
- `X-Content-Type-Options: nosniff`
- `Referrer-Policy: no-referrer`
## Wire Protocol (WebSocket)
### Host → Guest
| Message type | Payload fields | When sent |
|---|---|---|
| `sync` | `messages[]`, `state` (`isStreaming`, `model`, `cwd`, `sessionName`) | On connect, session switch, model change |
| `message_update` | `message: RenderMsg` | Streaming assistant turn (partial) |
| `message_end` | `message: RenderMsg` | Assistant turn finalized |
| `tool_start` | `toolCallId`, `toolName`, `args` | Tool execution begins |
| `tool_end` | `toolCallId`, `result`, `isError` | Tool execution completes |
| `agent_start` | — | Agent turn starts |
| `agent_end` | — | Agent turn ends |
| `status` | `clientCount` | Client disconnects |
### Guest → Host
| Message type | Payload | Effect |
|---|---|---|
| `prompt` | `text: string` | `pi.sendUserMessage(text)` — as normal message when idle, as follow-up when busy |
| `stop` | — | `ctx.abort()` — cancels the current agent turn |
**Per-connection server-side rate limit:** max 30 prompt messages per 60-second
sliding window; messages over 64 KB are silently dropped. The limit applies
per WebSocket connection — opening multiple connections multiplies it.
### RenderMsg shape
```typescript
interface RenderMsg {
id: string; // SessionEntry id, or "pending" while streaming
role: "user" | "assistant" | "tool_result";
text: string;
toolCalls?: Array<{ id: string; name: string; args: string }>;
toolName?: string; // tool_result only
toolCallId?: string; // tool_result only
isError?: boolean; // tool_result only
model?: string; // assistant only
}
```
## Session Lifecycle Events
```mermaid
flowchart TD
SS["session_start"] -->|--remote-control flag| START["startServer()"]
START --> SYNC0["sync all clients"]
SW["session_switch"] --> SYNC1["scheduleSync()"]
MS["model_select"] -->|when idle| SYNC1
AGs["agent_start"] --> B1["broadcast agent_start"]
AGe["agent_end"] --> B2["broadcast agent_end"]
MU["message_update"] --> B3["broadcast message_update"]
ME["message_end"] --> B4["broadcast message_end"]
TS["tool_execution_start"] --> B5["broadcast tool_start"]
TE["tool_execution_end"] --> B6["broadcast tool_end"]
SD["session_shutdown"] --> STOP["server.stop()"]
```