Docs / Platform architecture

Platform Architecture

Status: current implementation. This is the concise component map for the code in this repository. Fuller canonical descriptions, current checkpoint status, and unfinished operator work are tracked in internal engineering documents, not published here. Dated research and ADR files preserve earlier Rust, Worker/D1, Firecracker, Xorg, and rebuilt-Chromium proposals, but those proposals are not the implemented platform.

Topology

 console / SDK / MCP / extension / raw-CDP client
                         |
                 public HTTPS / WSS
                         |
             Cloudflare Tunnel terminates TLS
                         |
          +--------------v----------------+
          | gb-server: one static Go      |
          | control-plane binary          |
          |                               |
          | accounts/auth/API keys        |
          | console/team/Stripe           |
          | session scheduler and proxy   |
          | vault broker and approvals    |
          | metering/artifact index/jobs  |
          +--------+-------------+--------+
                   |             |
        bbolt or Postgres     gb-kmsd
        (durable store)       (sealed root key,
                   |           localhost bearer API)
                   |
     per-node bearer-authenticated HTTP/WS, or
     bearer + fingerprint-pinned HTTPS (real prod)
                   |
          +--------v---------+
          | gb-noded         |
          | (separate host   |
          |  in production)  |
          | actions/streams  |
          | CDP/recording    |
          +--------+---------+
                   |
       hosted: one headless Chrome process per
       allocation in a disposable Docker cell
                   |
       network=none; node-owned public egress proxy

 recording: node READY spool -> plane verifies -> filesystem or R2 -> ACK

The current hosted target is two DigitalOcean droplets, not a single consolidated VM: gb-server and gb-kmsd run together on the control-plane host, and a second, separate host runs the hosted-mode render node — a single 2-vCPU box cannot host both roles at once, since hosted mode's fixed per-session safety reserves alone (1.0 CPU per potential concurrent session + 0.5 CPU flat node reserve) already consume the entire budget on a 2-vCPU box before any browser cell runs. The control-plane host's own gb-noded is stopped and disabled by design — a self-mode registration is categorically excluded from hosted session scheduling (supportsHostedSessionProtocol requires Mode == "hosted"), so it was left running nothing rather than wasting the control plane's scarce capacity. Cloudflare provides the public tunnel/TLS boundary for the control plane's client-facing hop only. The application does not currently use Cloudflare Worker, D1, KV, or Durable Objects. The default application origin is https://app.getagentbrowser.com. Exact hosts and IPs are documented internally, not published here.

Control plane: gb-server

cmd/gb-server is the application gateway and control plane. It provides:

The durable store is an interface (internal/platform.Store) with two implementations: boltStore, backed by a single embedded bbolt file (normally /var/lib/agentbrowser/platform.db), used for local and self-hosted deployments; and pgStore, backed by Postgres, which is what the live hosted deployment actually runs (the control-plane host's server.env points DB_PATH at a postgres:// DSN, not a bbolt file path — a Postgres migration completed and cut over production data). Either backend contains accounts, workspaces, memberships, invitations, web sessions, API keys, usage, nodes, artifacts, jobs, webhooks, wrapped workspace keys, and sealed credential envelopes. Choosing pgStore is what makes horizontal, multi-instance gb-server replication possible; the current live deployment still runs exactly one gb-server process, so it is a single-process/durable-shared-database topology today, not an already-running replicated service.

Active browser-session routing is process-local. gb-server keeps the live session map, concurrency reservations, selected node endpoint, close-phase progress, pending vault approvals, and the broker's bounded recent audit window in memory. It reaps idle/aged sessions and drains on shutdown, but it does not adopt live sessions after its own restart. That limit matters operationally even though accounts, configuration, usage, and completed artifact indexes are durable.

Render plane: gb-noded

cmd/gb-noded has two deliberately different modes.

Hosted mode

Every normal or raw-CDP customer allocation owns a whole browser process. The node uses a process pool to launch one headless Chrome in one disposable Docker execution cell; BrowserContexts are not the hostile-tenant boundary. Hosted mode rejects a display and uses the digest-pinned Playwright chromium_headless_shell binary from build/browser-cell.Dockerfile.

The execution cell is implemented with a distinct non-root UID, read-only root filesystem, private mount/PID/network/IPC/cgroup namespaces, memory/CPU/PID limits, no_new_privs, the checked seccomp policy, and enforced docker-default AppArmor. The browser payload has no effective, permitted, inheritable, or ambient capabilities. SYS_CHROOT remains only in the bounding set for Chromium's inner sandbox. The node owns the rootful Docker control boundary; cells never receive the Docker socket.

Each cell has network=none and receives only a sealed private bind containing CDP and egress Unix sockets. Browser traffic exits through the node-owned proxy. The proxy accepts HTTP, HTTPS, CONNECT, and upgrades only after resolving a destination to entirely public addresses, rejects loopback/private/link-local/metadata targets, and pins a validated address for the dial. Hosted callers cannot supply another proxy.

The repository's real-cell gate proves these controls for the tested image/policy pair. It does not by itself publish a production image digest or prove a live hosted-cell deployment; the current handoff records that operator gap.

Self-hosted mode

Self mode launches a direct browser process and pools BrowserContexts. It may use an operator-provided display and is intended for trusted local or single-operator use. It is not described as execution-cell isolated. The self-hosted node still uses the same control-plane registration and node API today; a completely disconnected local control plane is not implemented.

The uinput and Xorg work preserved in cmd/gb-prove, cmd/gb-inputcheck, and the measurement logs is proof tooling and design evidence. It is not wired into the current hosted gb-noded service, so the hosted runtime must not be documented as an Xorg/inputd node.

Node ↔ plane protocol

Each node credential is unique and operator configured. gb-server binds it to an exact node ID, endpoint, mode, region, warm-pool size, and capacity. Registration and heartbeat requests authenticate with that credential, and the plane uses the same per-node credential for outbound RPC to the configured endpoint. One node cannot register another node's identity or receive RPC for another configured endpoint.

The direction is important: the node makes outbound register/heartbeat calls, but there is no persistent reverse control channel. The plane directly calls the node for allocation, actions, streams, CDP, recording pull, and delete.

The node listener defaults to loopback HTTP (127.0.0.1:8092) for local and self-hosted deployments, unchanged. It can also serve HTTPS (GB_NODE_TLS_CERT/GB_NODE_TLS_KEY), and the plane pins that exact certificate by SHA-256 fingerprint (tls_fingerprint in the node's NODE_CREDENTIALS entry) rather than trusting a CA chain — this is server-cert-pinned transport encryption, not mutual TLS, so the per-node bearer token remains the actual authentication. The real production render node (a separate host from the control plane) is configured this way, verified live 2026-08-17. A non-loopback node that instead sets the explicit, logged allow_insecure_transport: true opt-in is choosing plaintext HTTP on purpose; anything else non-loopback is rejected at startup. See Security & credential vault for the full detail.

Clients never need the node address. Normal agent traffic remains on the public control-plane origin. Raw-CDP clients receive a short-lived, session-bound ticket in the returned cdp_url; reusable account keys are accepted only in Authorization headers. The plane verifies the ticket/key and workspace before opening a bearer-authenticated WebSocket to the node.

Session lifecycle

  1. An API key requests a session. The plane derives the workspace from that key, checks plan/concurrency limits, reserves an allocation ID, and picks a compatible live node with capacity.
  2. The node allocates the exact requested ID. Hosted mode gives it a whole execution cell/process. If requested recording capabilities are missing, the plane discards the allocation instead of returning a degraded session.
  3. Commands, event streams, watcher streams, and raw-CDP bytes pass through the plane. Action, recording-finalization, and close operations are serialized per exact session on both services.
  4. A credential fill asks the node for the live page origin, then the plane broker checks key scope, origin allowlist, and any required human approval before decrypting and sending transient injection material to the node. See Security & credential vault for the full plaintext boundary.
  5. On close, the plane first persists a recording if one exists, then deletes the node session, writes idempotent instance_seconds usage, emits best-effort lifecycle notification, and removes its in-memory route. A phase failure remains retryable during that process lifetime.

Recording and artifact flow

The recorder publishes an immutable READY spool rather than a destructive one-shot download. The node atomically publishes and fsyncs a secret-free READY sidecar with its session ID, format version, relative archive path, exact length, and SHA-256. Repeated GETs before ACK return the same local tarball; startup revalidates and reindexes intact sidecars and quarantines corrupt, partial, traversing, or symlinked entries. gb-server pulls it through the authenticated node API, verifies both values while writing a temporary file, commits the object and bbolt index, then sends a checksum/size-bound ACK. Only ACK removes the node copy and sidecar.

Artifact storage is selected at startup:

Nodes do not upload directly to R2. Filesystem downloads do not use signed URLs. Artifact egress is not currently written as a usage event, so it is not accurate to describe every read as an egress meter.

The filesystem backend performs bounded object/index reconciliation at startup. R2 startup reconciliation is still unfinished. READY bytes and their sidecars survive a failed node drain on disk; the node revalidates and rebuilds its READY index on restart, quarantining invalid entries instead of serving them. An exact checksum/size ACK still owns cleanup of an intact quarantined tree.

What is durable, and what is not

internal/platform.Store rows below say "bbolt/Postgres" because the same durable state lives in whichever backend is configured: bbolt for local/self-hosted, Postgres for the live hosted deployment (see above).

StateCurrent locationRestart behavior
Accounts, workspaces, auth sessions, API keys, rolesbbolt/PostgresDurable
Wrapped keys, sealed secrets, approval-required flagsbbolt/Postgres + KMS serviceDurable at rest
Pending approvals and recent broker audit windowgb-server memoryLost on restart
Usage, jobs, webhooks, artifact metadatabbolt/PostgresDurable
Filesystem artifact objectsPlane recordings directoryDurable; reconciled at startup
R2 artifact objectsR2Durable; startup object/index reconciliation pending
Active plane session routing and close flagsgb-server memoryNot adopted after restart
Live browser/process stategb-noded memory + ChromeLost/reaped on process/host failure
READY recording lookupNode memory, bytes on local diskBytes remain; lookup recovery is manual after restart
Webhook deliveriesBounded in-memory admissionBest-effort; durable outbox pending

The current architecture favors a small, auditable operational surface. Future replication, durable routing, remote-node TLS/mTLS, R2 reconciliation, and outbox work should be described as future changes until their code, tests, and operator evidence land.