From 71e58c768ce8d7fd89c0be88ab030ca4d68745b8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 8 Dec 2025 21:50:16 +0100 Subject: [PATCH] docs: add control channel reference --- docs/control-api.md | 41 +++++++++++++++++++++++++++++++++++++++++ docs/remote.md | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 docs/control-api.md create mode 100644 docs/remote.md diff --git a/docs/control-api.md b/docs/control-api.md new file mode 100644 index 000000000..9f2dd2ca3 --- /dev/null +++ b/docs/control-api.md @@ -0,0 +1,41 @@ +# Control channel API (newline-delimited JSON) + +Endpoint: `127.0.0.1:18789` (TCP, localhost only). Clients reach it via SSH port forward in remote mode. + +## Frame format +Each line is a JSON object. Two shapes exist: +- **Request**: `{ "type": "request", "id": "", "method": "health" | "status" | "last-heartbeat" | "set-heartbeats" | "ping", "params"?: { ... } }` +- **Response**: `{ "type": "response", "id": "", "ok": true, "payload"?: { ... } }` or `{ "type": "response", "id": "", "ok": false, "error": "message" }` +- **Event**: `{ "type": "event", "event": "heartbeat" | "relay-status" | "log", "payload": { ... } }` + +## Methods +- `ping`: sanity check. Payload: `{ pong: true, ts }`. +- `health`: returns the relay health snapshot (same shape as `clawdis health --json`). +- `status`: shorter summary (linked/authAge/heartbeatSeconds, session counts). +- `last-heartbeat`: returns the most recent heartbeat event the relay has seen. +- `set-heartbeats { enabled: boolean }`: toggle heartbeat scheduling. + +## Events +- `heartbeat` payload: + ```json + { + "ts": 1765224052664, + "status": "sent" | "ok-empty" | "ok-token" | "skipped" | "failed", + "to": "+15551234567", + "preview": "Heartbeat OK", + "hasMedia": false, + "durationMs": 1025, + "reason": "" // only on failed/skipped + } + ``` +- `relay-status` payload: `{ "state": "starting" | "running" | "restarting" | "failed" | "stopped", "pid"?: number, "reason"?: string }` +- `log` payload: arbitrary log line; optional, can be disabled. + +## Suggested client flow +1) Connect (or reconnect) → send `ping`. +2) Send `health` and `last-heartbeat` to populate UI. +3) Listen for `event` frames; update UI in real time. +4) For user toggles, send `set-heartbeats` and await response. + +## Backward compatibility +- If the control port is unavailable (older relay), the client may fall back to the legacy CLI path, but the intended path is to rely solely on this API. diff --git a/docs/remote.md b/docs/remote.md new file mode 100644 index 000000000..195e3f8be --- /dev/null +++ b/docs/remote.md @@ -0,0 +1,38 @@ +# Remote mode with control channel + +This repo supports “remote over SSH” by keeping a single relay (the master) running on a host (e.g., your Mac Studio) and connecting one or more macOS menu bar clients to it. The menu app no longer shells out to `pnpm clawdis …`; it talks to the relay over a persistent control channel that is tunneled through SSH. + +## Topology +- Master: runs the relay + control server on `127.0.0.1:18789` (in-process TCP server). +- Clients: when “Remote over SSH” is selected, the app opens one SSH tunnel: + - `ssh -N -L :127.0.0.1:18789 @` + - The app then connects to `localhost:` and keeps that socket open. +- Messages are newline-delimited JSON (documented in `docs/control-api.md`). + +## Connection flow (clients) +1) Establish SSH tunnel. +2) Open TCP socket to the local forwarded port. +3) Send `ping` to verify connectivity. +4) Issue `health` and `last-heartbeat` requests to seed UI. +5) Listen for `event` frames (heartbeat updates, relay status). + +## Heartbeats +- Heartbeats always run on the master relay. +- The control server emits `event: "heartbeat"` after each heartbeat attempt and keeps the latest in memory for `last-heartbeat` requests. +- No file-based heartbeat logs/state are required when the control stream is available. + +## Local mode +- The menu app skips SSH and connects directly to `127.0.0.1:18789` with the same protocol. + +## Failure handling +- If the tunnel drops, the client reconnects and re-issues `ping`, `health`, and `last-heartbeat` to refresh state. +- If the control port is unavailable (older relay), the app can optionally fall back to the legacy CLI path, but the goal is to rely solely on the control channel. + +## Security +- Control server listens only on localhost. +- SSH tunneling reuses existing keys/agent; no additional auth is added by the control server. + +## Files to keep in sync +- Protocol definition: `docs/control-api.md`. +- App connection logic: macOS `Remote over SSH` plumbing. +- Relay control server: lives inside the Node relay process.