5.3 KiB
Protocol (English summary)
Canonical document: docs/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 ids (or name, raw values as-is). Items without id/name, or duplicate keys → send the whole value instead.