Architecture
AutoRune is a Bun/TypeScript monorepo. Everything runs on one server: a phone app talks to an API, the API runs one agent-runner per running avatar, and each runner puts a stock coding agent in a sandbox that plays the game through rs-sdk.
The whole stack
flowchart TB
subgraph Phone["Phone browser"]
APP["autorune-app<br/>React + Chakra UI (Vite PWA)"]
end
subgraph Public["Public websites"]
HS["autorune-hiscores<br/>static React site"]
DOCS["docs site<br/>VitePress (this site)"]
end
DISCORD["Discord OAuth"]
PROVIDER["Owner's AI provider<br/>OpenRouter · Anthropic · OpenAI · Gemini<br/>or Claude plan via Claude Code"]
subgraph Server["Server (nginx + TLS in front)"]
API["autorune-api<br/>Bun + Hono · REST + WebSocket"]
PG[("Postgres<br/>via Drizzle")]
subgraph Runner["agent-runner (one process per running avatar)"]
CLIENT["Game client<br/>lite (unwatched) or headless Chromium (watched)"]
GLUE["Autonomy loop · chat inbox/guard<br/>skill broker · video encoder"]
BRIDGE["Gateway bridge · egress proxy<br/>hiscores/skills gate"]
subgraph Sandbox["Per-avatar sandbox (bubblewrap + cgroup)"]
ENGINE["Engine<br/>opencode or Claude Code"]
MCP["rs-sdk MCP server<br/>execute_code · list_bots · get_market"]
SDK["rs-sdk SDK (bot / sdk)"]
end
end
subgraph World["rs-sdk world (unmodified submodule)"]
GW["rs-sdk gateway"]
LC["LostCity game engine"]
FR["friends server (PMs)"]
end
end
APP -- "HTTPS + WSS (chat, thoughts, video)" --> API
APP -. "sign in" .-> DISCORD
HS -- "public /hiscores API" --> API
API --- PG
API -- "spawn + IPC" --> GLUE
ENGINE --> MCP --> SDK
SDK -- "only its own bot" --> BRIDGE
ENGINE -- "allowlisted host only" --> BRIDGE
BRIDGE --> GW
BRIDGE -. "CONNECT :443" .-> PROVIDER
CLIENT --> GW
CLIENT --> LC
GW --> LC
LC --- FR
API -. "reads player saves" .-> LC
Reading it from top to bottom:
- App. A mobile-only PWA (React + Chakra UI, built with Vite). It shows the live game view, the chat overlay, the thought stream and a settings sheet. On a desktop it shows "open on your phone" with a QR code.
- API.
autorune-api(Bun + Hono) handles sign-in, avatars, AI providers, preferences, skills and the public hiscores. A WebSocket per watched avatar carries chat, status, thoughts and video. It owns the avatar lifecycle: it starts and stops runners and brings running avatars back after a restart. - Postgres. Users and identities, refresh tokens, avatars (with sealed game credentials and the owner's desired run state), encrypted AI provider configs, preferences and the skill library. The API accesses it through Drizzle ORM with SQL migrations. (Owner ↔ avatar chat history is still kept in a simple file store from Phase 1.)
- agent-runner. One Bun process per running avatar, spawned by the API and connected to it over Bun's IPC channel. It owns the bot's only game client, runs the autonomy loop, polls in-game chat, brokers skill-library requests, encodes video, and starts and supervises the sandbox.
- Sandbox. A bubblewrap sandbox in its own systemd scope with memory, task and CPU limits. It holds the avatar's workspace and the engine. Its only network is loopback ports that the runner forwards. See Security.
- Engine. The stock coding agent:
opencode serveorclaude -pwith stream-json. It uses rs-sdk's own MCP server and rs-sdk's ownAGENTS.md/CLAUDE.md, plus one appended AutoRune section. - rs-sdk. The SDK connects as a controller through the gateway. The runner's game client (lite or browser botclient) actually executes the actions. See rs-sdk and us.
- World. The LostCity engine, rs-sdk's gateway and rs-sdk's friends server, all bound to loopback and run from the pinned
vendor/rs-sdksubmodule.
Repository layout
| Path | What | Built with |
|---|---|---|
apps/autorune-app | The phone app | React 19, Chakra UI v3, Vite, Vitest, Playwright |
apps/autorune-api | REST + WebSocket API, lifecycle, hiscores | Bun, Hono, Drizzle ORM, zod |
apps/autorune-hiscores | Public hiscores website | React, Chakra UI, Vite (same stack as the app) |
apps/autorune-docs | This documentation site | VitePress, Mermaid |
services/agent-runner | Per-avatar runner, sandbox, engine glue | Bun, bubblewrap, systemd scopes |
packages/shared | Wire contracts shared by API, runner and app | zod schemas |
packages/contracts, services/indexer | Placeholders for the planned on-chain ownership | (empty) |
infra/ | nginx vhost, systemd units, world config, installers, deploy scripts | shell |
vendor/rs-sdk | rs-sdk, pinned and unmodified | git submodule |
docs/ | Internal dev notes (the detailed source for this site) | Markdown |
How a message flows
When you type "Goal: get 20 fishing" in the app:
- The app sends
chaton its WebSocket. The API checks you own the avatar, keeps the message in the chat history and forwards it to that avatar's runner over IPC. - The runner's autonomy loop sees a
Goal:line, records an owner goal, writes it into the avatar'sTASK.mdand prompts the engine. - The engine thinks, then calls rs-sdk's
execute_codewith a short script. The script'sbot.*/sdk.*calls go through the gateway bridge to the gateway, and the game client carries them out in the world. - Reasoning, text and tool calls stream back as thoughts (runner → API → your socket). The turn's final reply becomes a chat line.
- When the turn ends, the loop sends the next automatic prompt, and so on until the goal is done. Then the avatar chooses the next one.
How video flows
The runner owns one game client per bot. Unwatched, that is rs-sdk's headless lite client (no rendering, about 65 MB). When you open the app, the runner switches to a headless Chromium botclient. A WebCodecs VP8 encoder inside that page encodes the game canvas. Chunks go runner → API over IPC → your WebSocket as binary messages. The app decodes them with WebCodecs into a canvas. Browsers without VP8 decoding get JPEG frames. Details: Video streaming.
Sign-in
Discord OAuth2 with PKCE, scope identify only. The API issues a short-lived access token (15 minutes, kept in memory by the app) and a rotating refresh token in an httpOnly cookie scoped to the API path. WebSockets open with a single-use ticket. Only invited Discord accounts can sign in today. Details: Security.
Deployment
- nginx terminates TLS (Let's Encrypt via certbot) and serves the static sites (app, hiscores, docs). It proxies
/apiand the API host to the API. - systemd runs the long-lived parts as units: the API, the LostCity engine, the rs-sdk gateway and the friends server. Runners are child processes of the API. Sandboxes are transient systemd user scopes.
- Postgres runs natively on the host, with a separate database and role per environment.
- Only a TEST environment exists today. Production is planned. See Operations.