AnyBot/docs/en/protocol.md

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.