Tier 1 — Code agent
Python or Node evaluation, plus shell commands run directly (not spawned from within that code), inside a gVisor container. Sub-second and low cost.
Tier 1 runs model-authored code in a language runtime, inside a gVisor sandbox. It is the tier for "compute this", and it is where most agent execution belongs.
The restriction people expect — "Tier 1 can't run a shell command at all" — is narrower than it
sounds, and was itself tightened over time: a plain shell command (kind: command, run directly by
the platform's own agent, not spawned by your code) runs fine here. What's actually blocked is a
process already running inside the sandbox spawning a subprocess of its own.
Capability
| Executes | Python 3.12, Python 3.11, Node 22, Node 20 (kind: code); a plain shell command run directly (kind: command), not spawned from within running code |
| Does not execute | A subprocess spawned from inside running code — os.system, subprocess.Popen, child_process.spawn |
| Filesystem | Ephemeral, writable /tmp and /workspace, discarded at session end unless a volume is attached |
| Network | Default-deny, opt-in by policy allowlist |
| Packages | Preinstalled per template; runtime install only when the policy allows a registry destination |
| Resources | 1–4 vCPU, 256 MiB–8 GiB |
| State | Optional across runs in the same session; process state resets per run by default |
Process creation from inside a running kind: code invocation is blocked at the sandbox boundary —
os.system, subprocess.Popen, child_process.spawn all fail with EPERM there. This is not a
missing binary — it is a denied syscall. A detached process started directly (POST /sessions/{id}/start, outside any running code) is unaffected: it is the platform's own agent
spawning the process, not a subprocess of the sandboxed interpreter.
Runtime: gVisor
Tier 1 runs in a gVisor sandbox. gVisor interposes a user-space kernel (the sentry) between the guest process and the host kernel, implementing the Linux syscall surface in Go. The guest never issues a raw syscall to the host.
Why gVisor and not a plain container: the host kernel is the only shared trust boundary in a container, and the Linux syscall surface is far too large to defend against code an attacker influences. Why gVisor and not a microVM: start time. A sentry plus an overlay mount is tens of milliseconds; a microVM is hundreds.
Hardening on top of the sandbox: read-only rootfs with a per-session overlay, a dropped capability
set, no CAP_NET_RAW, a cgroup v2 CPU and memory limit with OOM reported as a run failure rather
than a host event, and no access to host devices other than /dev/null, /dev/zero, /dev/urandom.
Latency
| Warm p50 | 90 ms |
| Warm p95 | 250 ms |
| Cold p95 | 900 ms |
The warm path claims an already-started sentry from the pool and binds a fresh overlay. The cold path starts a sentry and pulls the template layers if the host does not already have them cached. Template locality is the main driver of cold-start variance; see scheduler and warm pools.
Metering
| Meter | Unit | Notes |
|---|---|---|
tier1.session_s | seconds | Wall clock in running |
cpu_ms | milliseconds | Actual CPU consumed, not allocated |
mem_gib_ms | GiB-milliseconds | Allocated memory integrated over time |
egress_gb | GiB | Through the egress proxy |
cold_start | count | For visibility, not billed |
Because cpu_ms is actual rather than allocated, idle time inside a tier 1 session is cheap. Holding
a session open between runs in a loop is usually the right call.
Working with it
One-shot
curl -sS "$WB_BASE_URL/runs" \
-H "Authorization: Bearer $WB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"tier": 1,
"runtime": "python3.12",
"code": "import pandas as pd; print(pd.read_json(\"/workspace/in.json\").describe())",
"files": [{ "path": "/workspace/in.json", "content_b64": "…" }],
"timeout_ms": 15000
}'
Session with state across runs
SESSION=$(curl -sS "$WB_BASE_URL/sessions" -H "Authorization: Bearer $WB_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "tier": 1, "template": "tpl_base_python_3_12", "persist_process": true, "idle_timeout_s": 120 }' \
| jq -r .id)
# run 1 defines a variable
curl -sS "$WB_BASE_URL/sessions/$SESSION/exec" -H "Authorization: Bearer $WB_API_KEY" \
-H "Content-Type: application/json" -d '{ "code": "df = load()" }'
# run 2 sees it
curl -sS "$WB_BASE_URL/sessions/$SESSION/exec" -H "Authorization: Bearer $WB_API_KEY" \
-H "Content-Type: application/json" -d '{ "code": "print(df.shape)" }'
persist_process: true keeps one interpreter alive across runs, which is the notebook-like model
most agent loops want. It also means one run's leaked state is visible to the next — do not use it
for multi-tenant code.
Templates
| Template | Contents |
|---|---|
tpl_base_python_3_12 | CPython 3.12, stdlib, httpx, pydantic |
tpl_data_python_3_12 | Above plus pandas, numpy, scipy, matplotlib, pyarrow, duckdb |
tpl_base_node_22 | Node 22, undici, zod |
tpl_data_node_22 | Above plus danfojs, apache-arrow, d3 |
Private templates built on these bases are supported, with the caveat that a template outside the warm pool pays the cold path. Pin a private template to a warm pool if it is on a latency-sensitive path.
A pip install inside a run costs network egress, several seconds, and a policy exception for the
registry. If a package is used often, bake it into a private template instead.
When to move up to tier 2
- The work needs
git,docker, a package manager, or any CLI. - The code genuinely needs to spawn processes.
- The workspace must survive as a real filesystem across many sessions with a volume.