AnyBot/docs/en/protocol.md

68 lines
5.3 KiB
Markdown

# Protocol (English summary)
Canonical document: [docs/protocol.md](../protocol.md) (Chinese). For anyone modifying the relay or writing another client. All replies are JSON; errors are `{"error": "plain sentence"}` with an HTTP status.
## Identity
| Who | Proves itself with | From |
|---|---|---|
| Person (phone) | `Authorization: Bearer <session>` | `POST /relay/login` |
| An AnyBot (host) | `Authorization: Bearer <host token>` | `POST /relay/hosts` (after login) or `POST /relay/pair/claim` (pairing code) |
The server stores only hashes. Passwords are argon2id (legacy PBKDF2 hashes still sign in; upgraded on next password change).
## Relay endpoints
### Account
| Method | Path | Notes |
|---|---|---|
| POST | `/relay/signup` | `{user, password, invite}`. First account needs no invite; later ones need `ANYBOT_RELAY_INVITE` |
| POST | `/relay/login` | `{user, password, device}` → `{token}`; with 2FA on → `{need_2fa, challenge}` (valid 5 min). Lockout 8 tries / 5 min per IP+account |
| POST | `/relay/2fa/verify` | Second step. Sign-in: `{challenge, code}` → `{token}` (TOTP code, or a one-time recovery code → adds `used_recovery`). Enrollment (with session): `{code}` activates the new secret |
| POST | `/relay/2fa/enroll` | (session) → `{secret, uri, recovery_codes}` — codes shown once; 2FA is on only after a verified code |
| POST | `/relay/2fa/disable` | (session) `{code}` turns 2FA off |
| POST | `/relay/sessions/logout` | (session) kicks **every** device of the account, including this one |
| POST | `/relay/logout` | ends this session |
| GET | `/relay/me` | account, `two_factor`, usable AnyBots (`online`, `uses`, `hands`), signed-in devices, backup presence |
### Hosts and proxying
| Method | Path | Notes |
|---|---|---|
| POST | `/relay/hosts` | (person) `{name}` → `{id, token}`; re-registering a name rotates the token |
| POST | `/relay/hosts/<id>/remove` | (person) remove a host |
| GET | `/relay/host/ws` | (host) the long connection — see wire format below |
| GET | `/relay/host/next` · POST `/relay/host/reply/<id>` | legacy long-polling for hosts without WebSocket |
| GET | `/relay/host/peers` · GET/POST `/relay/host/members` | other hosts; team roles (admin/member) |
| POST | `/relay/host/notify` | push notification to the account's phones |
| POST | `/relay/host/sites/<name>` | publish a bot-made website at `/s/<name>/` |
| PUT/GET | `/relay/backup` | encrypted backup; the server cannot open it |
Proxying: `/h/<host>/api/...` (phone → that AnyBot, role owner/admin/member); `/relay/hosts/<host>/api/sync[/...]` (host → brain's `/api/sync…`, role `mirror`); `/relay/hosts/<host>/api/device/run` (host → hands, role `device`). The relay injects `user`, `role`, `from`. Offline → 503 immediately; no answer in 35 s → 504. VNC streaming: `POST /relay/screen-ticket {host}` → 10-minute ticket; `WebSocket /relay/vnc/<host>?t=|?ticket=` carries binary VNC bytes and typed text.
## AnyBot's own API (`remote::handle`)
Reachable only through the relay (or on the LAN with the phone password). Highlights: `GET /api/state?since=` (bots, activity, pending approvals; long-poll ≤20 s), `GET /api/bots/<id>`, `POST /api/bots/<id>/<action>` (`send`, `stop`, `answer`, `read`, `new`, `upload`…), `GET/POST /api/screen/mac` (drive this Mac from the phone, if enabled), `/screen` + `/api/vnc` (view the bot's computer, noVNC), and the brain-sync family: `GET /api/sync?since=`, `POST /api/sync/edit` (`{ops, client, seq}`), `/api/sync/act`, `/api/sync/upload/<bot>`, `GET /api/sync/file`, `POST /api/sync/models`, `POST/GET /api/sync/screen` (thumbnails; `{kind:"hold"|"wake"}`).
## Wire format (`src/wire.rs`)
WebSocket binary frames, each message: `[4-byte big-endian header length][header JSON][raw body]` — bodies are not base64'd; `z: true` means the body is deflate-compressed, `z_ok: true` says the asker understands compressed replies.
| `t` | Direction | Header fields | Meaning |
|---|---|---|---|
| `hello` | AnyBot → relay | `brain`, `hands` | which brain this machine follows; is it hands |
| `job` | relay → AnyBot | `id, method, path, query, user, role, from, z, z_ok` | a request for this machine |
| `reply` | AnyBot → relay | `id, status, ctype, headers, z` | answer to a job |
| `call` | AnyBot → relay | `id, to, method, path, query, z` | ask another host (only `/api/sync…`, `/api/device/run`) |
| `ret` | relay → AnyBot | `id, status, ctype, z, headers` | answer to a call |
| `vopen` / `vdata` / `vtext` / `vclose` | both | `sid`, `why` | VNC session lifecycle: open, bytes, typed text, close |
Both loops poll every 2 ms while busy, 40 ms idle (`wire::Pace`), then flush queued messages; keepalive via WebSocket ping/pong. On disconnect the relay answers pending jobs 503, the AnyBot fails pending calls.
## Sync ops (`/api/sync/edit`)
`set` (one key; `next_*` counters only grow), `patch` (map/list item-level), `merge` (providers/skills/mcp_servers on move), `bot_add` (id collision → new id), `bot_del` (primary bot protected), `bot_set`, `bot_patch` (routines keep the brain's run times), `bot_read`, `bot_order`.
`patch` shapes: `{"value": …}` replaces; `{"map": {"put": {…}, "del": […]}}`; `{"list": {"put": […], "del": [keys], "order": [keys]}}` where keys are item `id`s (or `name`, raw values as-is). Items without id/name, or duplicate keys → send the whole value instead.