docs: rewrite README and add architecture docs
This commit is contained in:
parent
63a879046b
commit
54267f1842
35
README.md
35
README.md
|
|
@ -1,5 +1,9 @@
|
|||
# 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
|
||||
|
||||
```bash
|
||||
|
|
@ -11,7 +15,34 @@ pi install https://github.com/goofansu/pi-remote-control
|
|||
Run `/remote-control` to open the menu:
|
||||
|
||||
- **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)
|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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()"]
|
||||
```
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue