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:
- Human signup, password login, email verification/reset, revocable web sessions, account export/deletion, team invitations, workspace roles, and the web console.
- Hashed, revocable, workspace-scoped agent API keys and named permission scopes.
- Stripe plan, checkout, portal, and signed-webhook endpoints when Stripe is configured; usage and plan enforcement remain in the Go service.
- Render-node registration, heartbeat tracking, capacity-aware session placement, and authenticated proxying of actions, live streams, recordings, and raw CDP.
- The credential broker, bounded in-memory human approval queue and recent audit window, metering ingestion, scheduled jobs, outbound webhooks, and recording metadata.
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
- 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.
- 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.
- 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.
- 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.
- On close, the plane first persists a recording if one exists, then deletes the node session, writes idempotent
instance_secondsusage, 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:
- With complete R2 configuration,
gb-serveruploads the verified bundle to R2. Authenticated downloads redirect to a one-hour pre-signed GET. - Otherwise, the default filesystem backend stores the bundle below the recordings directory and
gb-serverstreams authenticated downloads.
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).
| State | Current location | Restart behavior |
|---|---|---|
| Accounts, workspaces, auth sessions, API keys, roles | bbolt/Postgres | Durable |
| Wrapped keys, sealed secrets, approval-required flags | bbolt/Postgres + KMS service | Durable at rest |
| Pending approvals and recent broker audit window | gb-server memory | Lost on restart |
| Usage, jobs, webhooks, artifact metadata | bbolt/Postgres | Durable |
| Filesystem artifact objects | Plane recordings directory | Durable; reconciled at startup |
| R2 artifact objects | R2 | Durable; startup object/index reconciliation pending |
| Active plane session routing and close flags | gb-server memory | Not adopted after restart |
| Live browser/process state | gb-noded memory + Chrome | Lost/reaped on process/host failure |
| READY recording lookup | Node memory, bytes on local disk | Bytes remain; lookup recovery is manual after restart |
| Webhook deliveries | Bounded in-memory admission | Best-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.