Choose where sandboxes run

Run sandboxes on this machine, in your own AWS, Google Cloud or Modal account, or on E2B, Daytona or Modal. Check the host, pick a runtime, and set the default.

A sandbox runs in one place. Pick it with on= in the SDK (local= in cua-sandbox) or --on in the CLI. Leave it out and the sandbox runs on this machine.

WhereonWhat you set up
This machinelocal (the default)A container engine, QEMU or Lume, with cua runtime setup
Your own cloudaws, gcp, modalcua cloud connect
Another providere2b, daytona, modalThe provider's API key, and a cua build with --features contrib
A machine that runs cua-spacesddirect:<host:port>Attach to or run cua-spacesd

To change where sandboxes run by default, see Change the default.

This machine#

Local sandboxes run on this machine with no account. The SDK picks the kind and runtime from the image and starts it itself.

ImageRuntime (auto)
Container image (any OCI rootfs, Image.linux(), python:3.12-slim)gvisor when installed, else runc, on Docker or Podman
Linux or Windows VM (-disk containerDisk, kind="vm", qcow2, ISO)qemu: KVM on Linux, HVF on macOS, WHPX on Windows, TCG otherwise
macOS VM (Image.macos())lume, Apple silicon only (the SDK starts lume serve)
Linux arm64 VM on Apple silicon without QEMUlume

Check and set up the host#

cua runtime doctor              # what this host can run (read-only)
cua runtime setup --dry-run     # the install plan
cua runtime setup               # install what is missing; or name qemu, lume, container

cua doctor checks the host, an image and a sandbox's guest in one report.

Not supported locally: macOS containers, macOS images that are not Lume images, and hosts with no container engine (install Colima or Docker Engine). Image.windows() is amd64 and runs under slow TCG emulation on Apple silicon.

Pick a kind or runtime#

cua sb create linux --name dev                       # a gVisor container
cua sb create linux --kind vm --name dev-vm          # its -disk variant under QEMU
cua sb create linux --runtime runc --name dev-runc   # a container without gVisor
cua sb create macos:tahoe --name mac                 # a Lume VM
cua sb rm dev

--runtime takes auto, gvisor or runc for containers and qemu or lume for VMs; a runtime implies its kind. A combination that does not exist fails with InvalidPlacement and lists the valid values:

invalid placement: runtime qemu runs vm sandboxes, not container ones locally; valid runtime: auto, gvisor, runc

In code, pass kind= and runtime= (Sandbox.create(image, kind="vm", runtime="qemu")); cua-sandbox still accepts a Runtime object (runtime support).

What differs locally#

  • The host's architecture. Others run emulated and slowly.
  • Services and public URLs are served on loopback (public URLs by the cua daemon, which the SDK starts).
  • Sidecars need runtime="runc" where gVisor would run.
  • Image layers build into your container engine before boot. VM images apply them after boot through cua-spacesd.
  • A sandbox runs until you delete it. keep_alive raises.
  • Images without cua-spacesd fall back to the runtime's agentless path (QMP, VNC, SSH or ADB) where there is one.

Images, VM disks and logs take disk space under ~/.cua. See Disk usage and cleanup.

Your own cloud#

Sandboxes and Spaces can run in a cloud account you own: --on aws (EC2), --on gcp (Compute Engine) or --on modal (Modal Sandboxes). The same images, SDK calls and cua-bench runs work there, and you pay the cloud directly.

Each sandbox dials out to the cua.ai relay as a machine of your account, so nothing in your cloud listens on the internet. Your laptop, your agents and the Cua Spaces app reach it wherever they are. Streams, files and presence pass through relay.cua.ai, and only your devices and the accounts you share a Space with can reach the sandbox. The guest holds only its own machine token, never your account credentials. An account holds at most 32 machines, and each cloud sandbox uses one until you delete it.

Everything here creates resources that your cloud account bills for. Cua gives each one a time limit (8 hours by default) and cleans up after itself, but set a budget in your cloud first.

What you need#

  • The cloud's own CLI, signed in. Cua uses its credentials where they are and stores only the names:
    • AWS: a profile in ~/.aws (access keys, SSO or an assumed role).
    • Google Cloud: gcloud auth login and a project with the Compute Engine API enabled.
    • Modal: a profile in ~/.modal.toml (modal token new).
  • A cua.ai sign-in (cua auth login). Each cloud sandbox joins the relay as a machine of your account.

Connect a cloud#

cua cloud status shows each cloud, whether its credentials were found on this machine, and what it can run at what hourly cost:

cua cloud status

cua cloud test checks an account without creating anything. It checks the credentials, runs a dry run of a create, and reads the quota, the region and the machine types:

cua cloud test aws --profile default --region us-west-2

cua cloud connect runs the same checks and connects only when they pass. --default also makes the cloud the default location, so cua sb create and create_space go there when you give no location:

cua cloud connect aws --region us-west-2 --default
cua cloud connect gcp --project my-project --region us-central1
cua cloud connect modal --environment sandboxes

--ttl-hours sets how long each sandbox there lives before it deletes itself (default 8; 0 keeps it until you delete it; Modal caps a sandbox at 24):

cua cloud connect aws --ttl-hours 2

The Cua Spaces app does the same from New Space, Your cloud, Connect a cloud.

Create sandboxes and Spaces#

A sandbox:

cua sb create linux --on aws --name research
cua sb exec aws:research -- uname -a
cua sb delete aws:research

From Python, including cua-bench runs:

from cua_sandbox import Image, Sandbox
 
async with Sandbox.ephemeral(Image.linux(), on="aws") as sb:
    print(await sb.shell.run("uname -a"))
    await sb.screenshot()
cb run cua-bench-basic --on aws

A Space is a cloud sandbox shown as the relay machine it joined as, relay:<machine>. Stop and start keep its disk (on AWS and Google Cloud):

cua spaces create linux --on aws --name research
cua spaces stop relay:cloud-0123456789abcdef
cua spaces start relay:cloud-0123456789abcdef
cua spaces delete relay:cloud-0123456789abcdef --force

Agents use the same create_space tool with on="aws", and cloud_status, cloud_test, cloud_connect and cloud_sweep from the Spaces contract.

What Cua creates#

CloudPer sandboxShared
AWSOne EC2 instance and its disk. Docker runs the image on it.One security group with no inbound rules, in the region's default VPC.
Google CloudOne Compute Engine instance and its disk. Docker runs the image on it.A network with no inbound rules, so the project's default firewall rules never apply to it.
ModalOne Modal sandbox running the image.Nothing.
  • Nothing else is created: no IAM users or keys, and no change to existing networks, security groups or firewall rules.

  • The VM installs only Docker. The image carries its own cua-spacesd.

  • Every resource carries these tags (labels on Google Cloud):

    TagValue
    cua-managedtrue
    cua-ownerThis cua home's owner id
    cua-spaceThe sandbox's name
    cua-machineIts relay machine
    cua-created-byThe cua version
    cua-expiresWhen it expires (Unix seconds)
  • Cua records every resource in ~/.cua/cloud/state.json before the cloud call, with its cloud id once it has one.

Costs#

cua cloud status shows the estimated on-demand price of each image family per hour while it runs (compute and disk, not network). A stopped VM bills only its disk.

Cloudlinuxlinux-slim
AWSt4g.medium (arm64), about $0.04/hourt4g.small, about $0.03/hour
Google Cloude2-medium, about $0.04/houre2-small, about $0.03/hour
Modal1 core (2 vCPU) and 4 GiB, about $0.24/hourthe same
  • Windows and Omarchy are not offered yet. They run as VMs inside the cloud's VM, which needs a nested-virtualization machine type. cua cloud status says so next to them.
  • macOS is not offered. On AWS it would need an EC2 Mac dedicated host, which has a 24-hour minimum.

Notes for each cloud#

  • AWS:
    • One region per connection (--region, else the profile's). Profiles, SSO and assume-role profiles from ~/.aws work as the AWS CLI's do.
    • The instance allows only IMDSv2 with a hop limit of 1, so the sandbox's containers cannot read the instance's metadata (or its user data). It has no key pair and no instance profile.
    • A new account's vCPU quota may be low. cua cloud test aws does not read it yet, and a create past it fails with AWS's own message.
  • Google Cloud:
    • One project and zone per connection (--project, --region, --zone).
    • The instance has no service account, so nothing on it can call Google APIs. Its containers reach the metadata server only for DNS.
    • cua cloud test gcp says when the Compute Engine API is disabled.
  • Modal:
    • One environment per connection (--environment) and a ~/.modal.toml profile (--profile).
    • Sandboxes run under gVisor by default. --runtime microvm uses Modal's VM runtime. Neither has /dev/kvm, so only Linux images run.
    • A sandbox cannot be stopped and started, and lives at most 24 hours.
    • Cua talks to Modal through cua-modal-helper, which uses Modal's official Go SDK. Build it with go build in libs/cua/crates/cua-contrib/modal-helper, and put it next to cua, on PATH, or in CUA_MODAL_HELPER.

Time limits and cleanup#

Three things end a sandbox that nobody deleted:

  • It shuts itself down. A VM runs shutdown at its time limit and checks it again after every reboot or start. Shutting down deletes it (AWS terminates on shutdown; Google Cloud's run limit deletes it). Modal ends a sandbox at its timeout.
  • A failed create cleans up. Whatever it made is deleted, and its relay machine is removed.
  • cua cloud sweep lists expired sandboxes and anything Cua created whose sandbox is gone. With --delete it deletes them. --all includes every Cua resource in that cloud.
cua cloud sweep
cua cloud sweep aws --delete
cua cloud sweep --all --delete

The sweeper touches only a resource that the cloud shows with cua-managed=true and this home's cua-owner, and that this home recorded or that carries its owner id. It never matches by name. Untagged resources and other owners' resources are never deleted, whatever they are called.

cua cloud disconnect forgets a cloud and deletes nothing. It lists what Cua still records there.

Other sandbox providers#

The same images (the canonical ghcr.io/trycua/linux, or any registry image) also run on third-party sandbox platforms. The location is one more --on word: --on e2b, --on daytona, --on modal. Everything above the location works unchanged: services, sb.spacesd(), cua doctor and cua-bench.

Your own cloud needs no provider key, and its sandboxes join the cua.ai relay. Providers are opt-in. The default cua build knows the words and tells you how to get the provider. A build with --features contrib includes them. Modal works both ways: connect it as your own cloud and its sandboxes join the relay; with only a Modal key, the sandbox uses Modal's own tunnels.

Set up a provider#

cargo install --locked --path libs/cua/crates/cua-cli --features contrib
cua auth provider set e2b          # reads E2B_API_KEY from stdin, never echoed
cua auth provider ls               # where each key comes from, never the value

An environment variable wins over a stored key: E2B_API_KEY, DAYTONA_API_KEY, or MODAL_TOKEN_ID and MODAL_TOKEN_SECRET.

Modal documents no HTTP API, so --on modal goes through Modal's official Go SDK in a small helper, cua-modal-helper. Build it once and put it next to cua or on PATH (or point CUA_MODAL_HELPER at it):

(cd libs/cua/crates/cua-contrib/modal-helper && go build -o ~/.local/bin/cua-modal-helper .)

Create a sandbox on a provider#

cua sb create linux --on e2b --name demo
cua doctor e2b:demo --strict
cua sb delete e2b:demo

The ref is <provider>:<name> (e2b:demo, daytona:demo). --claim-ttl sets the platform's lifetime backstop (default one hour).

from cua_sandbox import Image, Sandbox
 
async with Sandbox.ephemeral(Image.linux(), on="daytona") as sb:
    await sb.files.write_text("/tmp/hello.txt", "hello from daytona")
    print(await sb.files.read_text("/tmp/hello.txt"))  # hello from daytona

cua-bench takes the same word: cb run task <task> --on e2b.

What happens to the image#

The SDK resolves and pins the image first, then maps it onto the platform:

ProviderImage stepCached byEntrypoint
E2Ba template built fromImagedigest, start command, shaperuns once at build (E2B snapshots it); the SDK installs the cua-spacesd token
Daytonaa snapshot from imageNamedigest, entrypoint, shaperuns in every sandbox, with the token in its environment
ModalFROM the pinned image, ENTRYPOINT []Modal's image cachethe image's ENTRYPOINT and CMD as the command, with the token in its environment

A moved tag gets a new template. Requests a provider cannot run fail before any API call, with a typed error: VM variants (--vm), images without an amd64 build, Windows and macOS images, network="none", sidecars, and (Daytona, Modal) private registries. Modal tunnels are fixed at create: declare every port with --port.

Ports and services#

Declared services and cua-spacesd (3211) are reachable through the provider's HTTPS URLs. sb.service(name).url() and sb.tunnel.forward(port) return them. Ports are public on the provider's proxy. cua-spacesd authenticates with its own token, so keep your other services authenticated too.

Support matrix#

Each provider's lane runs nightly: it creates ghcr.io/trycua/linux:24.04, runs cua doctor <ref> --strict --json inside it, and generates this table from the latest reports. A regression shows up here on the next run.

ProviderKindVerdictdisplayinputstreamingaudiofilesprocessteleporttunnelsmcpauthMeasured
e2bcontainernot measured----------never
daytonacontainernot measured----------never
modalcontainernot measured----------never

full: the desktop works (display, input through cua-driver, streaming, files, auth). container-only: cua-spacesd answers but the desktop does not. headless-only: no cua-spacesd answered. not measured: no run yet.

Change the default#

Without --on (or on= / local=) a sandbox runs on this machine. To default to your own cloud instead, set default.on:

cua config set default.on aws   # written to ~/.cua/config.toml
cua config list                 # each setting, its value and its source

The first match wins:

  1. --on, on= or local=
  2. CUA_DEFAULT_ON (one shell or CI job)
  3. default.on in $CUA_HOME/config.toml
  4. local

default.kind and default.runtime (CUA_DEFAULT_KIND, CUA_DEFAULT_RUNTIME) work the same way, and cua doctor shows each default with its source. Switch back with cua config unset default.on.