Skip to content

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.