From 7cf8e00fdfe7b57bfc6653ce256011951755181d Mon Sep 17 00:00:00 2001 From: AnyBot Dev Date: Mon, 9 Nov 2026 17:56:16 +0800 Subject: [PATCH] English entry point: README.en.md, docs/en/*, demo recording guide (P2) - README.en.md: full English front door (install table, quick start, deployment, security-in-one-paragraph, license) + a positioning table against OpenClaw / OpenHands / Khoj / UI-TARS - docs/en/{architecture,install,multi-device,protocol}.md: faithful English summaries of the Chinese docs, each linking to the canonical version - docs/demo/README.md: how to record the GIF/MP4 the README reserves space for - language switch line at the top of the Chinese README --- README.en.md | 118 ++++++++++++++++++++++++++++++++++++++++ README.md | 2 + docs/demo/README.md | 16 ++++++ docs/en/architecture.md | 48 ++++++++++++++++ docs/en/install.md | 27 +++++++++ docs/en/multi-device.md | 44 +++++++++++++++ docs/en/protocol.md | 67 +++++++++++++++++++++++ 7 files changed, 322 insertions(+) create mode 100644 README.en.md create mode 100644 docs/demo/README.md create mode 100644 docs/en/architecture.md create mode 100644 docs/en/install.md create mode 100644 docs/en/multi-device.md create mode 100644 docs/en/protocol.md diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000..c820548 --- /dev/null +++ b/README.en.md @@ -0,0 +1,118 @@ +# AnyBot + +[中文 README](README.md) · **English** + +Your own AI bot. It chats, and it *does things*: files, shell commands, your browser, your screen, scheduled jobs. Self-hosted — your data stays on machines you own. + +- Every bot can use a different model: xAI Grok, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama (local), or any OpenAI-compatible endpoint +- Desktop app (macOS, Linux) + mobile web app (install to home screen, push notifications) +- Multiple computers: one **brain**, the others are its **hands** — all linked through your own account server, no ports opened + +## Demo + + +📹 *No recording yet — see [docs/demo/README.md](docs/demo/README.md).* + +## How it compares + +| | AnyBot | OpenClaw | OpenHands | Khoj | UI-TARS-Desktop | +|---|---|---|---|---|---| +| Positioning | Personal bot across your machines | Agent gateway on your hardware | Coding agent in sandboxes | Personal knowledge/RAG assistant | GUI agent on one desktop | +| Chat with your own bots | ✅ multi-bot, per-bot model | ✅ via gateways | coding-focused | ✅ | — | +| Controls your real desktop/browser | ✅ + approvals, VNC takeover | via plugins | sandboxed, not your desktop | — | ✅ (its whole product) | +| Multi-device brain/hands/phone | ✅ built in, zero open ports | bring your own | — | server + apps | single machine | +| Bot's own sandboxed computer (Docker, optional gVisor) | ✅ | host shell | ✅ | — | — | +| Secrets never enter the model (vault fills forms directly) | ✅ | — | — | — | — | +| Phone web app + push, no app store | ✅ | community | — | ✅ | — | +| One binary, JSON files, no database server | ✅ | ✅ | — | needs services | Electron | +| License | Apache-2.0 | MIT | MIT | AGPL-3.0 | Apache-2.0 | + +AnyBot's bet: a bot that touches your real machines is only trustworthy when the **harness** — not the model — owns the safety gates: per-action approvals, a sensitive-click policy the model cannot talk its way past, secrets painted out of screenshots, and an audit trail that survives crashes. + +## Install + +Pick one row, paste it into a terminal. The script installs what's needed (system packages, Rust, Docker). + +| I want… | On which machine | Command | +|---|---|---| +| **Desktop app** | macOS, Linux with a screen | `curl -fsSL https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.sh \| sh` | +| **Account server** (+ brain + HTTPS) | A Linux host with a domain | `curl -fsSL https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.sh \| sh -s -- server` | +| **Add a computer as hands** | Any Linux (headless fine) | In AnyBot press “Add a computer”, paste the one-liner it shows | + +First run compiles on your machine (5–15 min); later updates take 1–2 min. Read the script first: [install.sh](install.sh). It installs into your user folders — no root. + +## Getting started + +**One computer:** install the desktop app → Settings › Models → paste an API key → chat. + +**Phone + several computers:** run the server once (asks only for URL, account, password; gets HTTPS by itself), open the URL on your phone and “Add to Home Screen”, then pair other computers with one-time pairing codes. Any Linux box can become the bot's own computer, running inside Docker (optionally gVisor). A Mac can be the brain instead of a server ([docs/multi-device.md](docs/multi-device.md), method B). No database to run: state is a handful of JSON files, crash-safe writes; back up two Docker volumes. + +## Updates, removal + +| What | Command | +|---|---| +| Update app / hands | re-run the install line | +| Update server | on the server: `cd ~/AnyBot && make server` → “keep settings, update, restart” | +| Uninstall the program | `cd ~/AnyBot && make uninstall` (your bots and chats are kept) | +| Stop a hands machine | `systemctl --user disable --now anybot` | + +## Troubleshooting + +```sh +cd ~/AnyBot && make doctor +``` + +It tells you what's missing. Common: `Killed`/`signal: 9` while compiling → add 2 GB swap; `requires rustc 1.95` → `rustup update stable`; Mac says “cannot verify developer” → right-click › Open once; phone says “brain offline” → the brain machine is asleep ([docs/multi-device.md](docs/multi-device.md)). + +## Build from source + +```sh +git clone https://code.30cm.net/daniel.w/AnyBot.git +cd AnyBot +make deps && make && make install # deps: system packages + Rust 1.95+ (one time) +``` + +`make run` · `make test` · `make doctor` · `make serve` (headless) · `make setup` (pair this machine) · `make server` · `make dist` · `make help`. Needs: Rust ≥1.95, a C compiler, git; on Linux also `pkg-config` + `libasound2-dev` (deps installs all). + +## Deployment + +| Method | For | Notes | +|---|---|---| +| **Docker Compose** (default) | server | `install.sh server`: relay + brain + Caddy (auto-HTTPS) containers; data in Docker volumes; Tailscale URLs skip Caddy ([compose file](deploy/server/docker-compose.yml)) | +| Unattended | servers, Ansible | `ANYBOT_SETUP_USER=… ANYBOT_SETUP_PASSWORD=… [ANYBOT_SETUP_DOMAIN=…] sh install.sh server` | +| Portainer/Coolify/Dokploy | existing PaaS | they take the standard `deploy/server/docker-compose.yml` — set `ANYBOT_USER`, `ANYBOT_PASSWORD`, `ANYBOT_RELAY_PUBLIC_URL` | +| **systemd** user service | hands, headless brains | `anybot setup` installs `anybot.service`; `loginctl enable-linger` to keep it running logged-out | +| **launchd** | Mac as brain/relay | see [docs/multi-device.md](docs/multi-device.md) | +| Gitea Actions | release builds on `v*` tags | see [docs/release.md](docs/release.md) | + +## Security in one paragraph + +Commands run in a macOS sandbox (deny ~/.aws, Keychain, AnyBot's own data; allow only the attachments dir); per-action approvals with a policy layer the model cannot negotiate; clicking anything labelled “send/pay/transfer” is asked *every time*, even in direct mode, with no grant that silences it; card numbers and OTPs in screenshots are painted black before the model sees them; everything read from outside is wrapped as `` data, never treated as orders; the bot's own computer is a Docker container (gVisor for a second wall); relay stores only hashes (argon2id) and supports 2FA with recovery codes; egress binds loopback + detected bridge gateways, never 0.0.0.0. Details: [docs/architecture.md](docs/architecture.md) (Chinese; [English summary](docs/en/architecture.md)). + +## Documentation + +| Doc | Contents | +|---|---| +| [docs/install.md](docs/install.md) ([en](docs/en/install.md)) | per-platform details, manual steps, FAQ | +| [docs/multi-device.md](docs/multi-device.md) ([en](docs/en/multi-device.md)) | brain / hands / screen: setup, pairing, offline | +| [docs/architecture.md](docs/architecture.md) ([en](docs/en/architecture.md)) | layers, data storage, security design | +| [docs/protocol.md](docs/protocol.md) ([en](docs/en/protocol.md)) | relay API and long-connection wire format | +| [docs/release.md](docs/release.md) | packaging, CI | +| [FEATURES.md](FEATURES.md) | every feature (Chinese) | + +## License + +Apache License 2.0 — see [LICENSE](LICENSE). Copyright the contributors (The AnyBot contributors). Calls to third-party services (xAI, OpenAI, …) go through their public APIs; follow each service's terms and your own key agreements. + +## Repository layout + +``` +install.sh one-line installer (app, hands, server) +src/ AnyBot: desktop app, headless, `anybot setup` +relay/ account server +mobile/ phone web app (single file, embedded) +box/ the bot's own computer (Docker container) +deploy/ per-platform install & deployment files +docs/ technical docs (docs/en/ = English) +bench/ computer-use benchmark (tasks, scoring, red-team suite) +``` diff --git a/README.md b/README.md index cb0bc26..01676d0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # AnyBot +中文 · [English →](README.en.md) + 你自己的 AI Bot。可以聊天,也能幫你動手做事:處理檔案、執行指令、操作瀏覽器和螢幕、跑例行任務。 - 每個 Bot 可以用不同的 AI:xAI Grok、OpenAI、Claude、Gemini、OpenRouter、OpenCode Go、Ollama(本機),或任何 OpenAI 相容的服務 diff --git a/docs/demo/README.md b/docs/demo/README.md new file mode 100644 index 0000000..d0983ff --- /dev/null +++ b/docs/demo/README.md @@ -0,0 +1,16 @@ +# Demo media + +The README shows a demo here. To record one: + +1. Start AnyBot (desktop) and a bot with a key; run at 1440p, hide notifications. +2. Record a 20–40 s clip of one complete task, e.g. "organise the Downloads folder" + (chat → plan → tool steps → files moved) and one computer-use task + (bot opens the browser, fills a form, stops before Send — the approval card + popping up **is** the product). +3. Export GIF: `ffmpeg -i clip.mov -vf "fps=12,scale=960:-1:flags=lanczos,split[a][b];[a]palettegen[p];[b][p]paletteuse" docs/demo/anybot.gif` + (keep it under ~8 MB; if bigger, cut to one task). +4. Name it `docs/demo/anybot.gif`. The README comment shows where to un-comment + the `` line. + +Preferred: one GIF (desktop, computer-use with the approval card) + one 30 s +MP4 (`demo.mp4`) of the phone app pairing a new machine. diff --git a/docs/en/architecture.md b/docs/en/architecture.md new file mode 100644 index 0000000..6eda767 --- /dev/null +++ b/docs/en/architecture.md @@ -0,0 +1,48 @@ +# Architecture (English summary) + +The canonical, up-to-date document is [docs/architecture.md](../architecture.md) (Chinese). This is a faithful, condensed English version for contributors who don't read Chinese. + +AnyBot is one Rust binary (`anybot`, egui/eframe UI) with three modes, plus a small account server (`anybot-relay`): + +| Mode | Command | Purpose | +|---|---|---| +| Window | `anybot` | desktop app | +| Headless | `anybot --serve` | servers, always-on machines, "hands" | +| Setup | `anybot setup …` | terminal pairing wizard (`src/setup.rs`) | + +## Layers + +``` +UI (src/app.rs, src/app/*) — chat, sidebar, settings, computers, file preview, brain mirror + │ │ +Turn loop (agent.rs) Remote (remote.rs, tunnel.rs, wire.rs) + call model, run tools, phone API, device pairing, + approvals, summarising brain sync, long WebSocket to relay + │ │ +Models Tools (tools.rs, cloudbox.rs, botbrowser.rs, computer.rs, +(providers) axread.rs/axwrite.rs, macapps.rs, vault*.rs, mcp.rs, memory.rs, + routines.rs, triggers.rs, channels.rs) +``` + +Key modules: `agent.rs` (one turn: system prompt, streaming, tool loop, approval gates, per-step model routing `StepPlan`), `providers.rs` (OpenAI-compatible/Anthropic/OpenCode… + streaming + retries + a vision helper for models that can't see images), `policy.rs` (allow/ask/deny per action), `computer.rs` (one `computer` tool: screenshot, click/type, plus semantic actions `menu_click`/`fill_form`/`wait_for`; macOS reads the real accessibility tree, screenshots are annotated with numbered marks), `cloudbox.rs`+`box/` (the bot's own Docker computer with desktop + browser), `vnc.rs` (view/take over the bot's computer through the relay), `httpd.rs` (tiny shared HTTP server), `mobile/index.html` (phone UI, single embedded file). + +## Storage + +All in the data dir (macOS `~/Library/Application Support/AnyBot/`, Linux `~/.local/share/AnyBot/`, `%APPDATA%\AnyBot\`; `ANYBOT_DATA_DIR` overrides): `state.json` (bots, chats, settings, keys — written via temp file + rename, never half-written), `attachments/`, `browser/` (bot browser profile), `oplog/bot-.jsonl` (append-only work log: every step written to disk *before* it runs; used to repair chats and offer a "continue" card after a crash), `image-notes.json`, `anybot.lock` (OS file lock: one AnyBot per data dir). + +## Security design + +- **Approval gates** (`policy.rs`): actions run directly, ask first, or are denied. "Direct mode" skips asks — except for the gates below, which have no grant. +- **Sensitive clicks** (labels containing send/pay/transfer/delete/publish…): asked *every time*, direct mode included — a harness policy the model cannot negotiate past. +- **Command sandbox (macOS)**: bot commands run under `sandbox-exec`; AnyBot's data dir, `~/.gnupg`, Keychain, `~/.aws` are unreadable/unwritable; only `attachments/` and `.ssh` are readable, nothing outside is writable. Model API keys are stripped from the environment. Documented exceptions: App Store app CLIs, `ps`/`top`, and a first-time-asked, pattern-filtered single `osascript -e`. +- **The bot's own computer**: a Docker container; with gVisor installed on the Docker host, isolation becomes two walls (virtual kernel + Chromium sandbox). On macOS a Lima VM running Docker+gVisor is provided (`deploy/mac/gvisor-vm.sh`). +- **Vault**: passwords, card numbers, CVCs never enter the model; AnyBot fills them into web forms directly. +- **Screenshots are painted before the model sees them**: card numbers and OTP boxes are masked black (`marks::mask_secrets`). +- **Everything read from outside** (screens, web pages, files) arrives wrapped in ``; the system prompt makes it data, never orders. The real wall is the harness gates, not the wording. +- **Relay**: stores only hashes (argon2id; legacy PBKDF2 still works, upgraded on next password change); optional 2FA (TOTP + one-time recovery codes); one-page logout for all devices; login lockout per IP+account (8/5 min). +- **Egress**: `--serve` binds loopback + detected Docker bridge gateways only, never 0.0.0.0 (`src/egress.rs`). +- **docker.sock** is not mounted into the bot's container: a whitelisting docker-socket-proxy sidecar is used instead; rootless Docker works via `DOCKER_SOCKET`. + +## The computer-use control chain + +Model gets a screenshot + numbered marks built from the accessibility tree (macOS) → clicks a number, or names it (`press_named`) → harness translates to an AX press (preferred) or a pixel click (fallback) → the diff of the control tree between looks (`marks::diff`) is appended so the model sees only what changed. `bench/computer-use/` measures tasks end-to-end (model inference is ~90% of wall time) and includes the R1–R7 red-team suite for the gates above. diff --git a/docs/en/install.md b/docs/en/install.md new file mode 100644 index 0000000..fe87bd7 --- /dev/null +++ b/docs/en/install.md @@ -0,0 +1,27 @@ +# Install details (English summary) + +Canonical document: [docs/install.md](../install.md) (Chinese). + +`install.sh` does four steps: system packages (per-distro names: apt/dnf/pacman/zypper), toolchain (rustup → `~/.cargo`; Docker for the server mode), sources into `~/AnyBot`, then build + install. No root for the app; it picks desktop vs. hands automatically (Mac / Linux-with-desktop → app, headless Linux → hands). Low-RAM machines compile with fewer parallel jobs (one per 2 GB). Env: `ANYBOT_DIR`, `ANYBOT_BRANCH`. + +## Requirements + +| Platform | Needs | +|---|---| +| All | Rust **≥ 1.95** via rustup (apt's is too old), git, make, C compiler | +| macOS ≥ 12 | Xcode Command Line Tools (`xcode-select --install`) | +| Linux | `build-essential pkg-config libasound2-dev` (+ X11/Wayland, notify, clipboard libs — install.sh installs them) | +| Windows | VS Build Tools (C++); `cargo build --release` works, installer packaging is not done | + +## Make targets + +`make deps` (system packages + Rust, one time) · `make` (build) · `make install` (Mac → `~/Applications/AnyBot.app`, Linux → `~/.local/bin/anybot` + menu entry) · `make run` · `make test` · `make doctor` (what's missing on this machine) · `make serve` (headless) · `make setup` (pair this machine to your brain) · `make server` (host the account server here) · `make dist` (release archives into `dist/`) · `make relay` (build the relay alone). + +## FAQ + +- **`Killed` / `signal: 9` while compiling** — out of memory; add swap: `sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile`. +- **`requires rustc 1.95`** — `rustup update stable`; don't use the distro's rustc. +- **`alsa` / `pkg-config` not found** — `make deps`. +- **Mac: "cannot verify developer"** — right-click the app › Open, once. +- **Server URL unreachable** — DNS A record? ports 80/443 open? `cd ~/AnyBot/deploy/server && docker compose logs caddy`. +- **Uninstall** — `make uninstall` removes the program, keeps your bots and chats. diff --git a/docs/en/multi-device.md b/docs/en/multi-device.md new file mode 100644 index 0000000..876b5b6 --- /dev/null +++ b/docs/en/multi-device.md @@ -0,0 +1,44 @@ +# Multi-device: brain, hands, screen (English summary) + +Canonical document: [docs/multi-device.md](../multi-device.md) (Chinese). + +One AnyBot in your account is the **brain**: it stores every bot, chat, memory and schedule, and does the thinking. Other machines are its **hands** (screen, input, files, browser), phones are web clients. Everything talks through your **account server** (relay); each machine opens an outbound long connection, so nothing needs open ports or a static IP. + +| Role | What it is | How to set | +|---|---|---| +| Brain | stores and thinks; keep it always on | logging into the account makes it the brain (default) | +| Screen | a machine with a window: shows and edits the brain's bots, and acts as hands | Settings › General › Phone & account › Brain › “use this as brain” | +| Hands | lets the brain's bots drive this machine | the “Add a computer” one-liner, or `anybot setup` | +| Phone | web app on the relay's URL | sign into the same account | + +## Method A — server as relay + brain (recommended) + +A Linux host (≥2 GB RAM) plus one of: a **domain** (A record, 80/443; HTTPS certificates issued automatically), a **Tailscale** HTTPS URL (`https://host.tailnet.ts.net`, no ports opened at all), or a **LAN IP** (self-issued, auto-renewed certificate — trust it once on each device; instructions in the Chinese doc). + +```sh +curl -fsSL https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.sh | sh -s -- server +``` + +It asks three questions (URL, account, password), installs Docker if needed, gets HTTPS, creates the account, starts the brain. Update later with `cd ~/AnyBot && make server` (“keep settings, update, restart”). Release archives (built by Gitea Actions on `v*` tags) avoid building on the server: download `AnyBot--server-.tar.gz`, unpack, `cd deploy/server && ./setup.sh`. + +## Method B — a Mac as the brain (home, Tailscale) + +1. `make relay`; install `anybot-relay` under `~/Library/Application Support/AnyBot/relay/` and load `deploy/mac/com.anybot.relay.plist` via `launchctl` (edit the URL inside first). +2. `tailscale serve --bg 8780` → you get `https://mac-mini.tailnet.ts.net`. +3. AnyBot › Settings › General › Phone & account → enter that URL, create the account, sign in. This Mac is now the brain; other devices pair to it like Method A. + +## Pairing computers (hands) + +“⋯ › Add a computer” on the phone (or Settings on the brain) shows a one-time pairing line like: + +```sh +curl -fsSL https://your-domain/…/install.sh | sh -s -- hands https://your-domain 482913 +``` + +The 6-digit code lives 10 minutes, works once, carries no password. The machine installs itself, registers a host token, starts `anybot.service`, and runs at boot (enable `loginctl enable-linger $USER` to keep it running when logged out). + +## When a device can't connect + +- “Brain offline” → the brain machine is off/asleep, AnyBot isn't running there, or it lost the relay connection (each side keeps an outbound connection; a network change recovers by itself in seconds). +- A hands machine offline → check `systemctl --user status anybot`; re-run the pairing line to re-pair. +- A screen machine keeps a read-only **brain copy** (`brain-copy.json`) so recent chats stay readable while the brain is away. diff --git a/docs/en/protocol.md b/docs/en/protocol.md new file mode 100644 index 0000000..701550e --- /dev/null +++ b/docs/en/protocol.md @@ -0,0 +1,67 @@ +# 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 ` | `POST /relay/login` | +| An AnyBot (host) | `Authorization: Bearer ` | `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//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/` | 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/` | publish a bot-made website at `/s//` | +| PUT/GET | `/relay/backup` | encrypted backup; the server cannot open it | + +Proxying: `/h//api/...` (phone → that AnyBot, role owner/admin/member); `/relay/hosts//api/sync[/...]` (host → brain's `/api/sync…`, role `mirror`); `/relay/hosts//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/?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/`, `POST /api/bots//` (`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/`, `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.