Tier 2 — CLI agent
Shell, allowlisted CLIs and a real workspace, inside a microVM per run. Sub-second and moderate cost.
Tier 2 gives the agent a shell and a real filesystem inside a microVM. It is the tier for software work: clone, build, test, migrate, inspect.
Capability
| Executes | Any binary, through a real shell — a CLI allowlist is designed but not yet enforced, see below |
| Filesystem | Full ext4 root plus /workspace, optionally backed by a persistent volume |
| Network | Default-deny, opt-in by policy; all egress through the proxy |
| Inbound | A declared port gets a signed capability URL under *.sandboxes.workspaces.build by default, or no token at all if marked public — see Filesystem and ports |
| Resources | 1–16 vCPU, 1–64 GiB, 10–500 GiB disk, optional GPU on request |
| Processes | Unrestricted inside the guest, including daemons |
| Snapshots | Filesystem and memory |
| Lifetime | Idle timeout 30 s–2 h, maximum lifetime up to 24 h |
The guest is a real Linux kernel. Inside it the agent has root. That root is confined to the guest.
Runtime: microVM
Each tier 2 session is a Firecracker-class microVM with a minimal device model: virtio-blk, virtio-net, virtio-vsock, a serial console, and nothing else. No USB, no PCI passthrough, no BIOS, no graphics.
Three properties matter:
- The VMM is itself confined. The Firecracker process runs under a jailer: unprivileged user, seccomp filter, own network namespace, own chroot, cgroup limits. A VMM escape lands somewhere with almost no privileges.
- There is no network path to the host. Guest to control-plane traffic goes over vsock to the node agent, not over TCP. Guest to internet traffic goes out through a tap device into the egress proxy's namespace.
- Starts come from snapshots, not boots. A booted microVM is a few hundred milliseconds. A restored one is tens of milliseconds, because the memory image is already correct.
The CLI allowlist
Today's deployment runs any command on Tier 2 with no allowlist check — the policy engine and exec interposer below are the designed destination, not something a project can configure yet. Treat this section as the intended shape, not current behaviour.
Tier 2's shell is real, but which binaries it may execute is policy. Enforcement is in the guest agent via an exec interposer, with a deny-by-default list per project.
# policy: pol_build_tools
cli_allowlist:
mode: allowlist
binaries:
- git
- node
- pnpm
- python3
- pytest
- make
- tar
deny_always:
- curl # force network through registered tools or allowlisted hosts
- ssh
- nc
An allowlist at tier 2 is the main tool for granting shell-shaped capability without granting everything. See policy engine.
A permitted interpreter can run anything. If python3 is allowed, so is everything Python can
do. Allowlists reduce accident and casual misuse; the microVM is what contains a determined
attacker.
Latency
| Warm p50 | 180 ms |
| Warm p95 | 600 ms |
| Cold p95 | 2.5 s |
Warm means a snapshot restore of a pre-booted microVM for that template and size class. Cold means a kernel boot plus init plus template materialisation.
Metering
| Meter | Unit | Notes |
|---|---|---|
tier2.session_s | seconds | Wall clock in running |
cpu_ms | milliseconds | Guest CPU time, from cgroup accounting |
mem_gib_ms | GiB-milliseconds | Allocated, integrated over time |
disk_gib_ms | GiB-milliseconds | Scratch disk, integrated over time |
egress_gb | GiB | Metered at the proxy |
volume_gib_month | GiB-month | Attached volumes, billed whether attached or not |
snapshot_gib_month | GiB-month | Compressed snapshot size |
gpu_s | seconds | When a GPU is attached |
Working with it
Exec, streaming
curl -N "$WB_BASE_URL/sessions/$SESSION/exec?stream=true" \
-H "Authorization: Bearer $WB_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "command": ["bash", "-lc", "pnpm install && pnpm test"], "cwd": "/workspace/app" }'
Server-sent events arrive as stdout, stderr and a terminal exit frame.
Interactive PTY
wss://connect.workspaces.build/v1/sessions/{id}/pty?token=wb_st_…
A multiplexed WebSocket carrying a real PTY, used by the dashboard terminal and by agents that need to drive an interactive program.
Expose a port
Declared once, at session creation — not a separate call, and not addable after boot. See
Filesystem and ports for the full recipe, including the no-token
public form.
curl -sS "$WB_BASE_URL/sessions" -H "Authorization: Bearer $WB_API_KEY" -H "Content-Type: application/json" \
-d '{ "tier": 2, "execution_mode": "long_running", "image": "'"$IMAGE"'",
"expose_ports": [{ "port": 5173, "protocol": "http" }] }'
Running a real browser
A headless browser is not a special mode — it's an ordinary binary, run the same way pytest or
git would be, since Tier 2 is a full general-purpose microVM. Confirmed live end to end, twice:
Chromium via Playwright, launched inside a real Kata microVM, navigating a real page through the
mandatory egress proxy — on both the dev cluster and production.
# image: any base with Chromium + the Playwright python package baked in
# (e.g. FROM mcr.microsoft.com/playwright/python:v1.48.0-jammy, then pip install playwright)
curl -sS "$WB_BASE_URL/sessions" -H "Authorization: Bearer $WB_API_KEY" -H "Content-Type: application/json" \
-d '{ "tier": 2, "execution_mode": "one_shot", "image": "'"$IMAGE"'",
"resources": { "vcpu": 2, "memory_mib": 2048 },
"egress": { "allow": ["example.com"] } }'
import os
from urllib.parse import urlparse
from playwright.sync_api import sync_playwright
# The proxy's credentials ride in https_proxy's URL (user:pass@host) -- Playwright does NOT parse
# that the way curl does; split it into separate username/password fields or the browser silently
# never connects.
u = urlparse(os.environ["https_proxy"])
proxy = {"server": f"{u.scheme}://{u.hostname}:{u.port}", "username": u.username, "password": u.password}
with sync_playwright() as p:
browser = p.chromium.launch(headless=True, args=["--no-sandbox", "--disable-dev-shm-usage"], proxy=proxy)
page = browser.new_page()
page.goto("https://example.com")
print(page.title())
The default profile (1 vCPU, 512 MiB) is too tight for Chromium to spawn its own renderer process —
it doesn't error, it just hangs forever creating a new page. Name real resources at boot
(resources.vcpu/resources.memory_mib) for any browser workload; 2 vCPU / 2 GiB is comfortably
enough.
Templates
| Template | Contents |
|---|---|
tpl_base_ubuntu_24_04 | Ubuntu 24.04, coreutils, git, curl, jq, build-essential |
tpl_dev_polyglot | Above plus Node 22, Python 3.12, Go 1.24, Rust stable, uv, pnpm |
tpl_dev_docker | Above plus a rootless container daemon for image builds |
tpl_dev_cuda | GPU template, CUDA 12 toolchain, for GPU-attached sessions |
Snapshot and restore
The pattern that replaces holding a session open:
# end of an agent turn
SNAP=$(curl -sS "$WB_BASE_URL/sessions/$SESSION/snapshot" \
-H "Authorization: Bearer $WB_API_KEY" -H "Content-Type: application/json" \
-d '{ "name": "turn-7", "include_memory": true }' | jq -r .id)
curl -sS -X DELETE "$WB_BASE_URL/sessions/$SESSION" -H "Authorization: Bearer $WB_API_KEY"
# start of the next turn, possibly hours later
curl -sS "$WB_BASE_URL/sessions" -H "Authorization: Bearer $WB_API_KEY" \
-H "Content-Type: application/json" \
-d "{ \"tier\": 2, \"restore_from\": \"$SNAP\" }"
With include_memory: true the restored session resumes with running processes intact — a dev
server still listening, a REPL still holding its state.
When to move up to tier 3
The work needs a rendered DOM. A headless browser can be installed in a tier 2 template, but tier 3 exists because browsers need a different egress posture, a different resource profile and a different recording pipeline. Running Chromium at tier 2 gets you none of that and costs more than tier 3 to operate.