---
title: "Environments"
description: "The five environment classes, what each one is for, how availability is resolved, and why a gated class fails closed."
---
A Worker declares which environment classes it needs. Before anything starts, control resolves whether this deployment and this workspace can supply them, and refuses with a typed reason if not.
## The classes
| Class | For | Status |
|---|---|---|
| `code` | a Linux shell with Node and Python: repositories, tests, patches, scripts | Available |
| `browser` | an isolated Chromium session: web flows, forms, screenshots | Available |
| `desktop` | a Linux desktop with approved applications | Beta |
| `android` | an emulator on selected device profiles | Beta |
| `ios` | the iOS Simulator | Waitlist |
Desktop, Android and iOS have **no backend in this build**. They are surfaced with their status, their allocation fails closed with a reason, and nothing about them is presented as working. The fleet did not change that: it stands behind `code` and `browser` and may not be named for the other three.
This page describes how an environment is resolved and what it enforces. It is not a statement that a run completes: as of 2026-09-10 one does not, for the reasons in [Overview](/workers/overview).
## Read `runnable`, not the label
```bash
curl -fsS "$WS/worker-environments" -H "Authorization: Bearer $OBOL_SESSION_TOKEN"
```
Each class carries six facts that together answer one question:
`available`, `beta`, `waitlist` or `unavailable`. What Obol offers as a product.
`configured` or `not_configured`. Whether *this deployment* has a backend for it.
Whether the workspace's plan includes the class.
The conjunction of the three above. **This is the answer.** A class can be `available` and not runnable because no backend is configured, and a surface that reads `availability` alone will offer a start button that fails.
`e2b`, `docker_dev`, `demo` or `none`. `none` is the absence of an implementation, and it can never report itself ready.
`none`, `development_only` or `vendor_attested_microvm`, ordered weakest first. The order is part of the contract, so two claims can be compared. There is deliberately no production member: no value here names an isolation boundary Obol has verified.
`allocates` or `fails_closed`.
`reason`, `backend_readiness_reason` and `entitlement_reason` carry the sentence to show a person.
## What the backends are
Code and browser can be stood behind one of four backends. Which one you get is a fact about your deployment, not about your plan, and control asserts none of them by default: a deployment that has configured nothing reports `backend: "none"`, `backend_readiness: "not_configured"` and `runnable: false` for every class.
### The Obol-operated E2B fleet — `e2b`
Linux microVMs for `code`, and `browser` through Playwright baked into a reviewed template. Obol operates the fleet on its own infrastructure account, and that account's credential buys compute and nothing else: it is never placed inside an environment, it never carries `e2b.*` connector traffic, and it appears in no receipt, snapshot or log. It is not a pooled vendor account, and it is a different object from the customer-facing `e2b` connector pack, which uses your own key.
Two operating rules are worth knowing because they are visible in behaviour. A guest is never publicly addressable — the vendor's default is the opposite, and for a class that can hold a login that default would put a logged-in browser on the open internet. And a sandbox is killed rather than paused, so `checkpoint` is unsupported on this backend: a paused guest at this vendor has no time to live and is never reaped, and a snapshot would leave run state there after the run that produced it ended, under no retention promise.
This backend's `isolation_claim` is `vendor_attested_microvm`: more than a development backend, and **not** production isolation, which nothing on this page claims. Your work — files, page contents and screenshots — sits inside a third party's infrastructure under Obol's account, and four things stay open about that:
- The microVM boundary and the compliance posture are the vendor's assertions. Obol has not verified either one.
- Domain-level egress is a routing control at that vendor, not a security boundary. Obol restricts outbound traffic with address rules and its own proxy instead, and never relies on domain matching.
- Separation between tenants sharing one fleet account is undocumented by the vendor.
- Some sandbox state can outlive the run that produced it, and no vendor retention promise currently covers it.
The fleet is decided in ADR-0104 (Accepted). Two contract details follow from what the vendor can express: `region` reports the deployment's configured region or `null` and never asserts a placement Obol did not choose, and `image_digest` carries an immutable build pin (`e2b:build:`) rather than a content digest. A mutable tag stays unrepresentable, which was the point of the original rule.
### The Obol-operated Browserbase browser fleet — `browserbase`
`browser` only, and it is the backend a deployment with both fleets runs
browser work on: a vendor-built Chromium with a real input transport, driven
over CDP from Obol's own process, where every classification, destination
check and mask happens before any byte reaches the vendor. Obol operates it on
its own infrastructure account under the same rules as the E2B fleet — the
credential buys browser compute and nothing else, never carries
`browserbase.*` connector traffic, and appears in no receipt or log. It is not
a pooled vendor account, and it is a different object from the customer-facing
`browserbase` connector pack, which uses your own key.
Two operating rules are worth knowing because they are visible in behaviour.
**Nothing persists at the vendor.** This backend never mounts the vendor's
persistent browser profiles (Contexts), so no login state outlives a session
and a session profile cannot be attached to a browser step here — a step that
needs a retained login runs on the E2B fleet instead. And **browser egress is
vendor-side**: pages reach the web on the vendor's network, which Obol cannot
restrict beyond main-frame navigations, so a `deny_all` browser step is
refused rather than accepted under a policy nothing enforces.
This backend's `isolation_claim` is also `vendor_attested_microvm`: more than a
development backend, and **not** production isolation. A dedicated VM per
session is the vendor's assertion, browser traffic egresses on the vendor's
network, and separation between tenants sharing one fleet account is
undocumented by the vendor and enforced by Obol. The decision, and the refusal
to mount Contexts that makes it defensible, is ADR-0106.
### The local Docker development backend — `docker_dev`
A development backend on a trusted host: shared kernel, no hypervisor boundary, no hardened host baseline, no attested image supply chain, no proven tenant reset. It is fit for development and demonstration. It is not production isolation, and its `isolation_claim` reads `development_only` rather than naming a stronger boundary.
### The demo backend — `demo`
Deterministic and Obol-owned. A demo run touches no customer credential and no real vendor, its estimate is a *known* zero rather than an unavailable one, and it never bills. Its claim is `development_only` too.
## When a class cannot run
Allocation is checked before anything chargeable starts, and the refusal is typed:
```json 409
{
"detail": {
"code": "environment_class_unavailable",
"reason": "environment_class_unavailable",
"what": "This run needs a desktop environment, which cannot start here.",
"why": "Desktop environments are in Beta and are not running in this deployment yet, so runs cannot start.",
"suggested_fix": "Run this Worker in a code or browser environment.",
"fix_action": { "action": "request_capacity", "environment_class": "desktop" },
"rule_reference": { "kind": "environment_availability", "id": null, "label": "Environment availability" },
"environment_class": "desktop",
"set_by": "control"
}
}
```
`set_by: "control"` is on every one of these. A failure reason is control's fact, never model prose.
The same `environment_class_unavailable` reason is currently returned when an unsupported *capability* is the real problem, because the closed failure taxonomy has no `capability_unsupported` variant yet. The `what` and `why` still describe what happened; the machine-readable reason is broader than the cause. This is recorded in `docs/workers/2026-09-09-self-serve-follow-ups.md`.
## Sandboxed commands on `code`
A code Worker can run commands in its isolated sandbox: `env_code_exec`, `env_code_file_read`, `env_code_file_write`. No credential is placed in the guest. Each command is a fresh `bash -c`, at most 240 seconds, with no background processes. Network scope for those commands is derived from the lease (`deny_all` → none; dependency provisioning or an allowlist → declared; anything else fails closed as undeclared). Control re-derives the same value and evaluates the stricter of the claimed and derived scope.
In `safe` mode, and in test mode, a sandboxed command whose network is none or declared is admitted without an approval hold. Other non-read environment commands still wait or are refused. The Read only preset permits no sandbox commands.
GitHub's API is not reachable from the sandbox. Clone public repositories over `https://github.com/…` and publish through gateway Git Data tools (`github.get_ref`, `github.create_tree`, `github.create_commit`, `github.create_ref`, then `github.create_pull`). Private repositories are not supported. A Worker acts in one environment class per run; code and browser together cannot run yet. Browser environments on Browserbase are unchanged.
Command output is an observation, not evidence of a vendor-side effect.
## Networking
A code environment denies egress by default except for dependency provisioning; a browser environment uses an allowlist. Widening it is a reviewed change to the Worker's limits, and a run that reaches a destination outside the list fails with `network_destination_denied` carrying a `widen_network_policy` fix.
Where the environment runs on the fleet, that policy is enforced in two places rather than one. `deny_all` is no internet access at the fabric, outside the guest. An allowlist is address rules whose only permitted destination is a SOCKS5 proxy **Obol operates**, and every name-based decision is taken there, where Obol can also enforce the scheme rule, refuse private addresses, pin the address it resolved and re-evaluate a redirect. The proxy fails closed when it is unreachable, which means its availability is your run's availability.
The vendor's own domain rules are not used at all, and that is deliberate rather than an omission: using any domain entry there permanently allows a public nameserver that cannot be removed, which would be a DNS channel to a third party inside an environment you have been told is restricted. With no domain entry the guest has no name resolution of its own, and the name it wanted arrives at the proxy for Obol to resolve and check. The per-command destination check still runs as well, because the vendor cannot express scheme restriction, private-address refusal, address pinning or redirect re-evaluation.
## Evidence from an environment
An environment produces observations: a screenshot, a command's output, a file. Those are recorded with their provenance, and provenance keeps two questions apart — who observed this, and what the observation is worth. Hosted compute raises no evidence ceiling: a screenshot of a confirmation page is an observation of a screen, and it is not proof that a vendor-side effect occurred. What a receipt may claim is decided by the route the call took, and the receipt names the class — see [Evidence](/receipts/evidence).