Architecture¶
Firebox is three pieces that talk over HTTP and don't share much else.
High-level¶
flowchart TB
subgraph Client["Caller side (your laptop / server / MCP host)"]
SDK["firebox.sandbox.Sandbox<br/>Python SDK"]
CLI["firebox CLI"]
MCP["MCP stdio server<br/>(Claude Desktop / Cursor)"]
end
subgraph Host["Daemon host (KVM-capable Linux)"]
Daemon["firebox-daemon<br/>FastAPI on :8765"]
Reg["In-memory registry<br/>+ TTL reaper"]
Auth["Token registry<br/>/etc/firebox/tokens.d/"]
subgraph VMs["Sandboxes"]
V1["microVM<br/>10.42.0.2"]
V2["microVM<br/>10.42.0.3"]
V3["microVM<br/>..."]
end
Daemon --- Reg
Daemon --- Auth
Daemon -.spawn.-> V1
Daemon -.spawn.-> V2
Daemon -.spawn.-> V3
end
SDK --HTTP + bearer--> Daemon
CLI --HTTP + bearer--> Daemon
MCP --HTTP + bearer--> Daemon
V1 -.fbr0 NAT.-> Net((Internet))
V2 -.fbr0 NAT.-> Net
Inside the daemon host¶
flowchart LR
subgraph H["Host"]
D["firebox-daemon<br/>(uvicorn)"]
BR["fbr0 bridge<br/>10.42.0.1/24"]
D --owns--> BR
end
subgraph V1["microVM 1"]
I1["/firebox-init<br/>(PID 1)"]
A1["agent.py<br/>:8500"]
C1["Playwright<br/>worker thread"]
I1 --> A1
A1 -. dispatch .-> C1
end
subgraph V2["microVM 2"]
I2["/firebox-init"]
A2["agent.py :8500"]
end
D -- HTTP /exec, /files, /browser --> A1
D -- HTTP --> A2
BR --- V1
BR --- V2
The in-VM agent.py runs as PID-1's child. It's a stdlib HTTP
server on :8500 that the daemon talks to over the fbr0 bridge.
Endpoints map 1:1 to SDK calls — /files/read, /processes/start,
/browser/navigate, etc. A dedicated worker thread owns the
Playwright session because sync_playwright is thread-pinned.
Sandbox lifecycle¶
sequenceDiagram
autonumber
participant C as Client
participant D as firebox-daemon
participant FC as firecracker
participant V as microVM init
participant A as agent.py :8500
C->>D: POST /sandboxes (token, template, ttl)
D->>D: auth + quota check
D->>D: allocate IP (atomic mkdir /srv/firebox/ips/N)
D->>D: create tap, sparse-cp template.ext4 → vm-X.ext4
D->>D: inject /firebox/agent.py + net.env
D->>FC: spawn (config-file)
FC->>V: kernel boot ~80 ms
V->>V: mount /proc, /sys, configure eth0
V->>A: exec python3 /firebox/agent.py
A-->>D: ready
D-->>C: { id, ip, expires_at }
loop Active session
C->>D: POST /sandboxes/{id}/run
D->>A: POST /exec { cmd, timeout }
A-->>D: { stdout, stderr, exit_code }
D->>D: touch (reset TTL)
D-->>C: result
end
Note over C,A: TTL elapses or POST /close
D->>A: POST /close (best-effort)
A->>V: reboot -f (PSCI / sysrq)
FC-->>D: process exits
D->>D: trap cleanup: drop tap, free IP, rm rootfs
Auth & quotas¶
flowchart TD
Req[Incoming HTTP request<br/>Authorization: Bearer X]
LoadTok[/Load all tokens<br/>/etc/firebox/tokens.d/*.json + legacy/]
Match{Match secret<br/>constant-time?}
Owner{Sandbox<br/>owned by token?}
Quota{Per-token + global<br/>quota OK?}
OK[Proceed]
F401[401 Unauthorized]
F403[403 Forbidden]
F429[429 Too Many Requests]
Req --> LoadTok
LoadTok --> Match
Match -- no --> F401
Match -- yes, admin --> OK
Match -- yes, scoped --> Owner
Owner -- no, write op --> F403
Owner -- yes / read op --> Quota
Quota -- exceeded --> F429
Quota -- within --> OK
Admin tokens bypass ownership and quota checks. Scoped tokens see only
their own sandboxes via GET /sandboxes and get HTTP 429 with a precise
reason ("would exceed max_mem_mib (2048 > 1024)") before any VM is
spawned.
Files on disk¶
| Path | What |
|---|---|
/srv/firebox/kernels/vmlinux-* |
Firecracker-CI kernels |
/srv/firebox/rootfs/base.ext4 |
Default Ubuntu 24.04 rootfs |
/srv/firebox/rootfs/ubuntu-*.squashfs |
Pristine source for rebuilding |
/srv/firebox/templates/*.ext4 |
User-built rootfs templates |
/srv/firebox/vms/vm-*.ext4 |
Per-sandbox writable overlay (transient) |
/srv/firebox/tmp/vm-*.json |
Per-sandbox Firecracker config (transient) |
/srv/firebox/ips/<N>/owner |
Atomic IP allocation locks |
/etc/firebox/token |
Legacy single-string admin token |
/etc/firebox/tokens.d/*.json |
Scoped tokens with quotas |
/etc/firebox/limits.json |
Optional global daemon caps |
/var/firebox-profiles/*.json |
Saved browser profiles (cookies + storage) |
Network topology¶
flowchart LR
Net[Internet] --> CF["Reverse proxy<br/>(Caddy / Cloudflared)"]
CF -- TLS terminated --> D["firebox-daemon :8765"]
subgraph H["Host"]
D
ETH[host eth0]
BR["fbr0<br/>10.42.0.1/24"]
D --- BR
ETH --- D
end
BR --- T1[tap-aaa]
BR --- T2[tap-bbb]
T1 --- VM1["microVM 1<br/>eth0 = 10.42.0.2"]
T2 --- VM2["microVM 2<br/>eth0 = 10.42.0.3"]
VM1 -. SNAT MASQUERADE .-> ETH
VM2 -. SNAT MASQUERADE .-> ETH
ETH --> Net
Each microVM gets a tap device on fbr0. Egress is NAT-MASQUERADE'd
through the host's default interface. Ingress for sandbox-served ports
is one iptables DNAT rule per --expose call.