Skip to content

Architecture

Writ edited this page Jul 28, 2026 · 1 revision

Architecture

                    ┌──────────────────────────────┐
  your browser ────►│ Coordinator                  │
       HTTPS        │ FastAPI · SQLite · scheduler │
                    │ serves the web UI            │
                    └───────────┬──────────────────┘
                                │ WebSocket dispatch
                    ┌───────────┴───────────┐
                    ▼                       ▼
             writ-agent-fleet        writ-agent-fleet
                    │                       │
       real browsers│                       │
                    ▼                       ▼
             target websites          doc-extract
                                (PDF · office · OCR)

Who does what

Coordinator. Holds the data and the truth. Serves the API and the built SPA, runs the scheduler, decides what should run and dispatches it. It launches no browsers — the image carries no Chromium, no Playwright, no xvfb.

Agents. Run the browsers, wherever you put them. They dial out over WebSocket, so they need no inbound ports and work behind NAT. Each advertises bounded capacity from its own resource governor.

doc-extract. Turns non-HTML bytes into text. Everything heavy in the deployment lives here — PDFium, ONNX Runtime, OpenCV, the OCR weights — which is exactly why the coordinator stays small and the agent stays a single binary.

Why the split

Three consequences fall out of it:

  1. Your IPs do the browsing. No shared proxy pool, no third party seeing your traffic.
  2. Scaling is horizontal and cheap. Add agents. The coordinator does not get busier because you crawl more.
  3. The blast radius is small. An agent handles hostile pages; the coordinator handling your database never touches one.

Note the arrow from agents to doc-extract: agents call it directly, with bytes they already fetched. The coordinator never does. It only hands each agent the address and secret at connect time.

Storage

One SQLite file plus a files directory, both on one volume. No external database, no Redis server — Redis-style needs (rate limits, presence, pub/sub, the scheduler's job store) are met by in-process fakeredis sharing one keyspace.

This is why the coordinator must run as a single worker: a second process gets its own empty keyspace, so token revocation stops working across workers and the scheduler double-fires.

Request paths

Path Purpose
/api/* The REST surface
/mcp Model Context Protocol endpoint
/ws/ai-gateway Agent dispatch WebSocket
/ws/record Live recording relay
/agent.sh Public, secret-free agent installer
/health Unauthenticated health JSON
/api/about Public AGPL §13 source offer
everything else The SPA history fallback

/agent.sh is declared before the SPA catch-all deliberately — otherwise curl … | sh would receive the HTML shell with a 200 and pipe a web page into a shell.

Repository layout

Path What it is
coordinator/ Python API and Alembic migrations
ui/ The built SPA the coordinator serves
frontend/ SPA source (shipped for AGPL source availability)
doc-extract/ Document and OCR service
connectors/writ-mcp/ Zero-dependency Node MCP bridge (MIT)
docker/ Dockerfiles, compose, entrypoint
scripts/ gen-env.sh and the CI check scripts

The Rust agent lives in its own repository, writ-agent.

Clone this wiki locally