Skip to content

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:

  1. 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.
  2. 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.
  3. 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.)
  4. 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.
  5. 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.
  6. Engine. The stock coding agent: opencode serve or claude -p with stream-json. It uses rs-sdk's own MCP server and rs-sdk's own AGENTS.md/CLAUDE.md, plus one appended AutoRune section.
  7. 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.
  8. 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-sdk submodule.

Repository layout ​

PathWhatBuilt with
apps/autorune-appThe phone appReact 19, Chakra UI v3, Vite, Vitest, Playwright
apps/autorune-apiREST + WebSocket API, lifecycle, hiscoresBun, Hono, Drizzle ORM, zod
apps/autorune-hiscoresPublic hiscores websiteReact, Chakra UI, Vite (same stack as the app)
apps/autorune-docsThis documentation siteVitePress, Mermaid
services/agent-runnerPer-avatar runner, sandbox, engine glueBun, bubblewrap, systemd scopes
packages/sharedWire contracts shared by API, runner and appzod schemas
packages/contracts, services/indexerPlaceholders for the planned on-chain ownership(empty)
infra/nginx vhost, systemd units, world config, installers, deploy scriptsshell
vendor/rs-sdkrs-sdk, pinned and unmodifiedgit 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:

  1. The app sends chat on 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.
  2. The runner's autonomy loop sees a Goal: line, records an owner goal, writes it into the avatar's TASK.md and prompts the engine.
  3. The engine thinks, then calls rs-sdk's execute_code with a short script. The script's bot.* / sdk.* calls go through the gateway bridge to the gateway, and the game client carries them out in the world.
  4. Reasoning, text and tool calls stream back as thoughts (runner → API → your socket). The turn's final reply becomes a chat line.
  5. 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 /api and 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.

AutoRune is an experiment built on rs-sdk and LostCity. RuneScape is a trademark of Jagex Ltd; this project is not affiliated with Jagex.