Sandbox, images and services
Sandbox, the sandbox() helper and its creation options, the Image builder, and the services, tunnels and MCP endpoints of a sandbox.
The Sandbox class is the entry point of cua-sandbox: create, connect to, or reattach a sandbox, then use its interfaces.
Local is the default: Sandbox.create(image) runs on this computer, on="aws", "gcp" or "modal" in your own cloud. local=False meant Cua Cloud, which ended on 18 October 2026: it fails with a clear error. Sandbox.connect(name) and the class methods take local=None: the local sandbox of that name when one exists.
Sandboxes#
Sandbox#
class Sandbox
Sandbox(transport: Transport, name: Optional[str] = None)A sandboxed computer environment.
Provides programmatic control of a VM or container through a unified
interface: .mouse, .keyboard, .screen, .clipboard,
.shell, .window, and .terminal.
Sandboxes are always isolated: they never control the host machine directly. To control the local machine, use cua-driver.
There are three ways to obtain a Sandbox:
- Persistent: provision and keep alive after the script exits:
sb = await Sandbox.create(Image.linux())
await sb.shell.run("whoami")
await sb.disconnect()- Connect: attach to an already-running sandbox by name:
sb = await Sandbox.connect("my-sandbox")
await sb.screenshot()
await sb.disconnect()- Ephemeral: auto-destroyed when the
async withblock exits:
async with Sandbox.ephemeral(Image.linux()) as sb:
await sb.shell.run("whoami")| Attribute | Type | Description |
|---|---|---|
name | Optional[str] | The sandbox name. |
telemetry_enabled | bool | Whether this sandbox records anonymous usage events. |
screen | Screen | Screenshots and screen size (cua-spacesd). |
mouse | Mouse | Pointer input in screen pixels (cua-spacesd). |
keyboard | Keyboard | Typing and key presses (cua-spacesd). |
clipboard | Clipboard | Clipboard text (cua-spacesd). |
shell | Shell | Run shell commands (cua-spacesd). |
files | Files | Read, write and transfer files (cua-spacesd). |
window | Window | The active window (cua-spacesd). |
terminal | Terminal | PTY terminal sessions (cua-spacesd). |
mobile | Mobile | Android touch and hardware keys (Android sandboxes). |
tunnel | Tunnel | Forward sandbox ports to loopback ports on this machine. |
services | Services | Older named-service helpers; prefer service(name). |
driver | Driver | Typed Cua Driver access (cua-sandbox[driver]). |
apps | Apps | Install and launch catalog applications (cua-spacesd). |
exposed_ports | dict | Map each Image.expose() port to the host port forwarding it. |
image_info | Optional[ImageInfo] | The image this sandbox runs, as resolved and pinned at create time (digest, variant, arch), or None when unknown: direct (URL) connections, images not resolved from a registry. |
location | str | Where it runs: local or direct. |
id | str | None | The qualified ref: local:<name> or direct:<host:port>. It is what Sandbox.connect(id) and Sandbox.delete(id) take, and what listings print. |
Sandbox.disconnect#
async def disconnect() -> NoneDrop the transport connection. The sandbox keeps running.
Sandbox.overlay#
async def overlay(
overlay: 'dict[str, Any] | list',
timeout: Optional[float] = None,
) -> listInjects binaries into this sandbox (see create(overlay=...)):
{"cua-driver": "./target/release/cua-driver"}. Returns one
cua.OverlayResult per binary (name, target, sha256,
previous_sha256, restarted); pass NAME=sha256:<hex> to
cua doctor --expect. timeout (seconds) bounds a cua-spacesd
restart.
Sandbox.info#
async def info() -> SandboxInfoPortable info: status (provisioning, starting, ready, stopped),
location (local or direct), services (name -> guest port),
expires_at and provider_details.
Sandbox.service#
def service(name: str) -> 'ServiceHandle'A named service (services={"mcp": 8765} at create):
request, url and public_url.
Sandbox.public_url#
async def public_url(
service: str,
*,
ttl: float = 3600,
label: Optional[str] = None,
) -> PublicUrlA shareable URL for service that stops working after ttl
seconds (60 s to 24 h): a loopback URL with its own token, served by
the cua daemon (started if needed).
Sandbox.revoke_public_url#
async def revoke_public_url(url: 'PublicUrl | str') -> NoneRevokes a URL from public_url (or its id).
Sandbox.destroy#
async def destroy() -> NoneDisconnect and permanently delete the sandbox (VM/container).
Sandbox.screenshot#
async def screenshot(
text: Optional[str] = None,
format: str = 'png',
quality: int = 95,
) -> bytesSandbox.screenshot_base64#
async def screenshot_base64(
text: Optional[str] = None,
format: str = 'png',
quality: int = 95,
) -> strSandbox.get_environment#
async def get_environment() -> strSandbox.get_display_url#
async def get_display_url(*, share: bool = False) -> strReturn a URL to view this sandbox's display.
With cua-spacesd this is a link to its HTML5 viewer, carrying a scoped viewer ticket that expires. Images without cua-spacesd return the page of a declared web display service, or a VNC address.
| Parameter | Type | Default | Description |
|---|---|---|---|
share | bool | False | If True, return a link meant for sharing (an expiring public link for a legacy web display service; viewer links always expire). If False, return a direct connection URL. |
Sandbox.viewer_url#
async def viewer_url() -> strA browser link to this sandbox's desktop in the cua-spacesd HTML5
viewer (video, audio, input, clipboard, files). The link carries a
scoped viewer ticket that expires after an hour. Same as
get_display_url() for sandboxes with cua-spacesd.
Sandbox.get_dimensions#
async def get_dimensions() -> tuple[int, int]Sandbox.mcp#
def mcp(service: str, *, path: str = '/mcp') -> AnyAn official MCP SDK client for the MCP server behind service.
An async context manager (pip install "cua-sandbox[mcp]"):
async with sb.mcp("mcp") as client:
tools = await client.list_tools()
result = await client.call_tool("add", {"a": 2, "b": 3})Requests go through the sandbox's service route (loopback), streamed both ways, so every protocol feature and content block works unchanged. No cua-spacesd needed.
Sandbox.mcp_config#
async def mcp_config(service: str, *, path: str = '/mcp') -> dict{"url": ..., "headers": {...}} of service's MCP endpoint, for
any MCP client (Claude Code, Cursor, other SDKs). The headers can carry
a short-lived bearer: fetch a fresh config per connection.
Sandbox.spacesd#
async def spacesd() -> AnyThe raw cua.SpacesdClient for this sandbox's cua-spacesd.
The typed escape hatch to every cua.env.v1 RPC (call_json),
process streaming, chunked transfers and media sessions. Raises
cua_sandbox._sdk.SpacesdNotAvailable when the sandbox has no
spacesd (for example a plain VNC-only image).
Sandbox.create#
@classmethod
async def create(
image: Image | None = None,
*,
pool: 'SandboxPool | str | None' = None,
name: Optional[str] = None,
on: Optional[str] = None,
local: Optional[bool] = None,
kind: Optional[str] = None,
runtime: 'Runtime | str | None' = None,
cpu: Optional[int] = None,
memory_mb: Optional[int] = None,
disk_gb: Optional[int] = None,
time_to_start: Optional[float] = None,
request_timeout: Optional[float] = None,
server_port: Optional[int] = None,
telemetry_enabled: bool = True,
command: Optional[list[str]] = None,
sidecars: Optional[list] = None,
env: Optional[dict[str, str]] = None,
services: Optional[dict[str, int]] = None,
wait_for: WaitFor = None,
memory: 'str | int | None' = None,
network: Optional[str] = None,
overlay: 'dict[str, Any] | list | None' = None,
) -> 'Sandbox'Provision a persistent local sandbox and return it connected.
Where, what kind and which engine are three separate choices:
on:"local".local=Trueis the same switch;onand a contradictinglocalis anInvalidArgument. Unset, the user default applies (CUA_DEFAULT_ON, elsecua config set default.onin$CUA_HOME/config.toml, else local, with a one-time notice thatCUA_QUIET_DEFAULT=1hides). Cua Cloud has ended:on="cloud",local=Falseandpool=naming no pool of this machine raiseCloudEnded.kind:"auto","container"or"vm". Unset: the image'skindwhen it has one, elsedefault.kind, else auto (macOS and Windows images are VMs, a container rootfs is a container, a disk-only image is a VM).runtime: the engine,"auto"or one the location offers for the kind:"gvisor"/"runc"(containers) and"qemu"/"lume"(VMs). Unset:default.runtime, else auto. A sandbox withsidecarsneeds"runc"where gVisor would run (separate gVisor containers cannot share a network namespace). ARuntimeobject (DockerRuntime(),TartRuntime(), ...) still picks a local adapter, as before.
A combination that does not exist (kind="container", runtime="qemu") raises InvalidPlacement, listing the valid
values.
Options:
command: argv replacing the image's entrypoint.env: environment variables.services: named guest ports,{"mcp": 8765}; reach them withsb.service("mcp"),sb.public_url("mcp")orsb.tunnel.forward(8765).wait_for:tcp("mcp")/http("mcp", "/health")(or a list); readiness otherwise is "the sandbox is running".memory:"4GB"/"512MB"(ormemory_mb).network:"default"(outbound network, like a Docker container) or"none"(no egress; the SDK still reaches the guest's published ports)."none"needs a local QEMU VM; containers and Lume raiseUnsupported.overlay: binaries to inject once it is up, so tests run the build under test and not the copy the image bundles:{"cua-driver": "./target/release/cua-driver"}(cua-driverandcua-spacesdresolve their guest path; other names take(path, "/guest/path")). Each replaces the guest file atomically, is recorded with its sha256 (cua doctor --expect) and what runs it is restarted. If it fails, the sandbox is deleted and the error raised. Seeoverlay.
pool: a pool of this machine (a SandboxPool from
create_pool, or its name) gives the sandbox the pool's image
and size and takes one of its ready sandboxes (about 0.1 s).
server_port names a port your image serves itself: it becomes the
readiness probe. Leave it unset for the default, daemon-agnostic
readiness (the provider reports the sandbox running; cua-spacesd on
3211 is used by the interfaces when the image has it).
Example
import asyncio
from cua_sandbox import Image, Sandbox, http
async def main():
sb = await Sandbox.create(
Image.from_registry("python:3.12-slim"),
command=["python", "-m", "http.server", "8000"],
services={"web": 8000},
wait_for=http("web", "/"),
cpu=2, memory="4GB",
)
print(sb.id)
await sb.destroy()
asyncio.run(main())Sandbox.connect#
@classmethod
def connect(
name: Optional[str] = None,
*,
url: Optional[str] = None,
token: Optional[str] = None,
api_key: Optional[str] = None,
local: Optional[bool] = None,
ws_url: Optional[str] = None,
http_url: Optional[str] = None,
container_name: Optional[str] = None,
cpu: Optional[int] = None,
memory_mb: Optional[int] = None,
disk_gb: Optional[int] = None,
telemetry_enabled: bool = True,
) -> '_ConnectResult'Connect to an existing sandbox by name.
Supports both await and async with. When used as a context
manager, disconnect() is called on exit: the sandbox keeps running.
Examples:
# plain await
sb = await Sandbox.connect("my-sandbox")
await sb.screenshot()
await sb.disconnect()
# context manager: disconnects on exit, sandbox keeps running
async with Sandbox.connect("my-sandbox") as sb:
await sb.screenshot()| Parameter | Type | Default | Description |
|---|---|---|---|
name | Optional[str] | None | A sandbox ref (local:<name>, direct:<host:port>, a legacy id) or a name. A cloud:<name> ref raises CloudEnded: Cua Cloud has ended. |
local | Optional[bool] | None | None (default): the local sandbox name. False raises CloudEnded. |
url | Optional[str] | None | A cua-spacesd URL to connect to directly (http://host:3211 or a relay URL). |
token | Optional[str] | None | The spacesd token for url. |
api_key | Optional[str] | None | Used as the spacesd token for url when token is not given. |
ws_url | Optional[str] | None | Removed (the computer-server WebSocket protocol is gone). |
http_url | Optional[str] | None | Base URL of a cua-spacesd (same as url). |
container_name | Optional[str] | None | Unused; kept for compatibility. |
Example
import asyncio
from cua_sandbox import Sandbox
async def main():
async with Sandbox.connect(url=URL, token=TOKEN) as sb: # or Sandbox.connect("my-sandbox")
result = await sb.shell.run("echo hello")
print([result.stdout, result.returncode, result.success])
asyncio.run(main())Sandbox.ephemeral#
@classmethod
async def ephemeral(
image: Image | None = None,
*,
pool: 'SandboxPool | str | None' = None,
name: Optional[str] = None,
on: Optional[str] = None,
local: Optional[bool] = None,
kind: Optional[str] = None,
runtime: 'Runtime | str | None' = None,
cpu: Optional[int] = None,
memory_mb: Optional[int] = None,
disk_gb: Optional[int] = None,
time_to_start: Optional[float] = None,
request_timeout: Optional[float] = None,
server_port: Optional[int] = None,
telemetry_enabled: bool = True,
command: Optional[list[str]] = None,
sidecars: Optional[list] = None,
env: Optional[dict[str, str]] = None,
services: Optional[dict[str, int]] = None,
wait_for: WaitFor = None,
memory: 'str | int | None' = None,
network: Optional[str] = None,
) -> AsyncIterator['Sandbox']A local sandbox that is deleted when the async with block exits.
Takes the same pool, on/local, kind, runtime, command,
env, services, wait_for, memory and network
options as create.
Sandbox.list#
@classmethod
async def list(
*,
location: Optional[str] = None,
local: Optional[bool] = None,
all: Optional[bool] = None,
api_key: Optional[str] = None,
) -> 'list[SandboxInfo]'List the local sandboxes (containers, QEMU, Lume, Android), each
with its location, kind and runtime.
| Parameter | Type | Default | Description |
|---|---|---|---|
location | Optional[str] | None | None (default) or "local": the local sandboxes. "cloud" raises CloudEnded: Cua Cloud has ended. |
local | Optional[bool] | None | The same switch as a bool (True is "local", False is "cloud"). |
all | Optional[bool] | None | Deprecated; everything is the default. |
api_key | Optional[str] | None | Unused; kept for compatibility. |
Sandbox.get_info#
@classmethod
async def get_info(
name: str,
*,
local: Optional[bool] = None,
api_key: Optional[str] = None,
) -> 'SandboxInfo'Get metadata for a specific sandbox.
| Parameter | Type | Default | Description |
|---|---|---|---|
name | str | required | Sandbox ref (local:<name>) or a name. A cloud:<name> ref raises CloudEnded: Cua Cloud has ended. |
local | Optional[bool] | None | None (default) or True: the local sandbox name. False raises CloudEnded. |
api_key | Optional[str] | None | Unused; kept for compatibility. |
Sandbox.suspend#
@classmethod
async def suspend(
name: str,
*,
local: Optional[bool] = None,
api_key: Optional[str] = None,
) -> NoneSuspend the local sandbox name, keeping its state;
resume continues it. Only that sandbox is affected.
The SDK suspends it on its backend (a container is paused or checkpointed, a QEMU VM snapshotted, a Lume VM stopped with its disk).
| Parameter | Type | Default | Description |
|---|---|---|---|
name | str | required | Sandbox name. |
local | Optional[bool] | None | None (default) or True: the local sandbox name. False raises CloudEnded: Cua Cloud has ended. |
api_key | Optional[str] | None | Unused; kept for compatibility. |
Sandbox.resume#
@classmethod
async def resume(
name: str,
*,
local: Optional[bool] = None,
api_key: Optional[str] = None,
) -> 'Sandbox'Resume the suspended local sandbox name and return it
connected. The backend resumes it.
| Parameter | Type | Default | Description |
|---|---|---|---|
name | str | required | Sandbox name. |
local | Optional[bool] | None | None (default) or True: the local sandbox name. False raises CloudEnded: Cua Cloud has ended. |
api_key | Optional[str] | None | Unused; kept for compatibility. |
Returns: A connected Sandbox ready to use.
Sandbox.restart#
@classmethod
async def restart(
name: str,
*,
local: Optional[bool] = None,
api_key: Optional[str] = None,
) -> 'Sandbox'Restart the local sandbox name (suspend then resume) and return
it connected (see suspend).
| Parameter | Type | Default | Description |
|---|---|---|---|
name | str | required | Sandbox name. |
local | Optional[bool] | None | None (default) or True: the local sandbox name. False raises CloudEnded: Cua Cloud has ended. |
api_key | Optional[str] | None | Unused; kept for compatibility. |
Returns: A connected Sandbox ready to use.
Sandbox.delete#
@classmethod
async def delete(
name: str,
*,
local: Optional[bool] = None,
api_key: Optional[str] = None,
) -> NonePermanently delete a local sandbox: stops the VM or container and removes the state file.
| Parameter | Type | Default | Description |
|---|---|---|---|
name | str | required | Sandbox name. |
local | Optional[bool] | None | None (default) or True: the local sandbox name. False raises CloudEnded: Cua Cloud has ended. |
api_key | Optional[str] | None | Unused; kept for compatibility. |
sandbox#
async def sandbox(
*,
on: Optional[str] = None,
local: Optional[bool] = None,
kind: Optional[str] = None,
ws_url: Optional[str] = None,
http_url: Optional[str] = None,
url: Optional[str] = None,
token: Optional[str] = None,
api_key: Optional[str] = None,
container_name: Optional[str] = None,
image: Optional[Image] = None,
runtime: 'Runtime | str | None' = None,
name: Optional[str] = None,
ephemeral: Optional[bool] = None,
cpu: Optional[int] = None,
memory_mb: Optional[int] = None,
disk_gb: Optional[int] = None,
) -> AsyncIterator[Sandbox]Async context manager for a sandboxed environment.
on/local, kind and runtime mean what they mean for
Sandbox.create.
.. deprecated:
Prefer `Sandbox.create()`, `Sandbox.connect()`, or
`Sandbox.ephemeral()` instead.SandboxInfo#
class SandboxInfo
SandboxInfo(
name: str,
status: str,
source: str,
os_type: Optional[str] = None,
host: Optional[str] = None,
vnc_url: Optional[str] = None,
api_url: Optional[str] = None,
created_at: Optional[str] = None,
id: Optional[str] = None,
location: Optional[str] = None,
services: dict = dict(),
expires_at: Optional[str] = None,
provider_details: dict = dict(),
kind: Optional[str] = None,
runtime: Optional[str] = None,
)Metadata for a local sandbox.
status from Sandbox.info is portable: provisioning,
starting, ready or stopped. Listings keep the provider's word
(running, suspended, ...). Provider internals (the local backend)
are in provider_details.
| Attribute | Type | Description |
|---|---|---|
name | str | |
status | str | |
source | str | |
os_type | Optional[str] | |
host | Optional[str] | |
vnc_url | Optional[str] | |
api_url | Optional[str] | |
created_at | Optional[str] | |
id | Optional[str] | |
location | Optional[str] | |
services | dict | |
expires_at | Optional[str] | |
provider_details | dict | |
kind | Optional[str] | |
runtime | Optional[str] |
Creation options#
Probe#
class Probe
Probe(service: str, path: Optional[str] = None)A readiness probe on a declared service: TCP, or GET path returning
2xx when path is set. Build it with tcp or http.
| Attribute | Type | Description |
|---|---|---|
service | str | |
path | Optional[str] |
Probe.native#
def native() -> AnyThe cua.ReadinessProbe for this probe.
http#
def http(service: str, path: str = '/') -> ProbeReady once GET path on the declared service returns 2xx.
tcp#
def tcp(service: str) -> ProbeReady once a TCP connect to the declared service succeeds.
PublicUrl#
class PublicUrl
PublicUrl(
url: str,
expires_at: str,
id: str,
service: str,
provider_details: dict = dict(),
)A shareable URL of a sandbox service that stops working at
expires_at: a loopback URL with its own token, served by the cua
daemon.
| Attribute | Type | Description |
|---|---|---|
url | str | |
expires_at | str | |
id | str | |
service | str | |
provider_details | dict |
Sandbox references#
AmbiguousSandbox#
class AmbiguousSandbox(LookupError)
AmbiguousSandbox(name: str, candidates: list[str])A bare sandbox name matches sandboxes in more than one location.
candidates are the qualified refs it matches (local:box,
relay:<id>); pass one of them.
The native SDK raises cua.CuaError.AmbiguousSandbox for the same
case.
| Attribute | Type | Description |
|---|---|---|
name | str | |
candidates | list |
Images and registries#
Image is an immutable, chainable description of what a sandbox boots. Builder methods return a new Image.
Builder methods add layers and return a new Image. Locally, a container image is built into your container engine before boot; it is cached by content. Layers a build cannot run (brew_install, choco_install, ...) are applied after boot through cua-spacesd, so they need an image that runs it.
Image builder#
Image#
class Image
Image(os_type: str, distro: str, version: str, kind: Optional[str] = None)Immutable, chainable image specification.
Each mutation method returns a new Image instance so that builders can be forked at any point.
| Attribute | Type | Description |
|---|---|---|
os_type | str | |
distro | str | |
version | str | |
kind | Optional[str] |
Example
from cua_sandbox import Image
img = Image.linux().apt_install("curl").pip_install("requests").env(DEBUG="1").expose(8080)
print(img.to_dict())
# {'os_type': 'linux', 'distro': 'ubuntu', 'version': '24.04', 'kind': None,
# 'layers': [{'type': 'apt_install', 'packages': ['curl']},
# {'type': 'pip_install', 'packages': ['requests']}],
# 'env': {'DEBUG': '1'}, 'ports': [8080]}Image.linux#
@classmethod
def linux(
distro: str = 'ubuntu',
version: str = '24.04',
kind: Optional[str] = None,
tier: Optional[str] = None,
) -> ImageLinux: the canonical ghcr.io/trycua/linux:<version> image.
kind=None (or "auto") runs what the image is (the rootfs: a
gVisor container); kind="vm" its -disk containerDisk (QEMU);
kind="container" the rootfs. Sandbox.create(kind=...)
overrides it.
tier: "full" (the default: dev tooling) or "slim"
(24.04-slim: cua-spacesd and Chromium only; what CI runs). A tier
CI has not published yet raises CuaError.ImageNotPublished; use
Image.from_registry(ref) to run it anyway.
Image.macos#
@classmethod
def macos(version: str = '26', kind: str = 'vm', tier: Optional[str] = None) -> ImagemacOS: the canonical ghcr.io/trycua/macos:<version> image (Lume locally).
Supported versions: "15" / "sequoia", "26" / "tahoe".
tier: "full" (the default), "slim" or "xcode" /
"xcode-<X.Y>" (full plus one pinned Xcode); unpublished tiers
raise CuaError.ImageNotPublished.
Image.windows#
@classmethod
def windows(
version: str = '2022',
kind: str = 'vm',
tier: Optional[str] = None,
) -> ImageWindows: the canonical ghcr.io/trycua/windows:2022 containerDisk.
Other versions, including "11", have no canonical image: they are
installed from a downloaded evaluation ISO. tier="slim" names
2022-slim once published.
Image.omarchy#
@classmethod
def omarchy(channel: Optional[str] = None) -> ImageOmarchy (Arch Linux, Hyprland) with cua-spacesd:
ghcr.io/trycua/omarchy:edge (or "rc"/"stable").
An amd64 VM: QEMU (emulated on arm64 hosts, so slow there). Until CI
publishes it this raises
CuaError.ImageNotPublished; use
Image.from_registry("ghcr.io/trycua/omarchy:edge", kind="vm") to
run it anyway.
Image.android#
@classmethod
def android(version: str = '14', kind: str = 'vm') -> ImageAndroid image. Always a VM (QEMU emulator).
Image.from_registry#
@classmethod
def from_registry(
ref: str,
*,
os_type: str = 'linux',
kind: Optional[str] = None,
agent_type: Optional[str] = None,
secret: Optional[Any] = None,
) -> ImageCreate an image from a registry reference.
secret (a cua_sandbox.RegistrySecret) pulls a private
image. It is never logged or saved.
os_type selects the firmware: Windows guest disks are built UEFI-only,
so a Windows containerDisk pulled from a registry must say so or it is
handed BIOS and will not boot. kind is resolved after pull when omitted.
agent_type names the guest control server the disk already runs, the
same hint as from_file: "osworld" selects the OSWorld Flask
server on port 5000 instead of the computer-server on 8000, for local
QEMU.
Image.from_file#
@classmethod
def from_file(
path: str,
*,
os_type: str = 'windows',
kind: str = 'vm',
agent_type: Optional[str] = None,
) -> ImageCreate an image from a local disk, ISO file, or URL.
Supported formats: qcow2, vhdx, raw, img, iso. URLs (http/https) are downloaded automatically. Zip files are extracted. For ISOs, the runtime will create a qcow2 disk and attach the ISO as a CD-ROM for installation/boot.
| Parameter | Type | Default | Description |
|---|---|---|---|
path | str | required | Local file path or URL (http/https). |
os_type | str | 'windows' | OS type hint ("linux", "windows", "macos", "android"). |
kind | str | 'vm' | "vm" or "container". |
agent_type | Optional[str] | None | Agent type hint (e.g. "osworld" for OSWorld Flask server). |
Image.from_dict#
@classmethod
def from_dict(data: Dict[str, Any]) -> ImageReconstruct an Image from a serialized spec dict.
Example
from cua_sandbox import Image
base = Image.linux().apt_install("git")
dev = base.pip_install("rich")
same = Image.from_dict(dev.to_dict()).to_dict() == dev.to_dict()
print([same, len(base.to_dict()["layers"]), len(dev.to_dict()["layers"])])
# [True, 1, 2]Image.apt_install#
def apt_install(*packages: str) -> ImageInstall packages via apt (Linux only).
Image.brew_install#
def brew_install(*packages: str) -> ImageInstall packages via Homebrew (macOS).
Image.choco_install#
def choco_install(*packages: str) -> ImageInstall packages via Chocolatey (Windows).
Image.winget_install#
def winget_install(*packages: str) -> ImageInstall packages via winget (Windows).
Image.apk_install#
def apk_install(*apk_paths: str) -> ImageInstall APK files via adb (Android only).
Image.pwa_install#
def pwa_install(
manifest_url: str,
package_name: Optional[str] = None,
keystore: Optional[str] = None,
keystore_alias: str = 'android',
keystore_password: str = 'android',
builder: str = 'pwa2apk',
push_timeout: Optional[float] = None,
) -> 'Image'Build an APK from a PWA manifest URL and install it (Android only).
Two builder backends are supported:
"pwa2apk"(default): generates a lightweight WebView-based APK. No Chrome dependency, no "Running in Chrome" banner, no Digital Asset Links needed. ~10 KB APK."bubblewrap": generates a Chrome Trusted Web Activity (TWA) APK. Requires Chrome on the device and a matching/.well-known/assetlinks.jsonon the server. Shows a mandatory "Running in Chrome" privacy disclosure on every launch.
| Parameter | Type | Default | Description |
|---|---|---|---|
manifest_url | str | required | Full URL to the PWA's manifest.json or manifest.webmanifest. Example: "http://10.0.2.2:3000/manifest.json" |
package_name | Optional[str] | None | Android package ID. Defaults to a reversed-hostname derivation (e.g. "com.example.app"). |
keystore | Optional[str] | None | Path to a *.keystore / *.jks file. When omitted a fresh keystore is generated and cached. Pass the keystore bundled with your PWA repo so the fingerprint is deterministic. |
keystore_alias | str | 'android' | Key alias inside the keystore (default "android"). |
keystore_password | str | 'android' | Password for both the store and the key (default "android"). |
builder | str | 'pwa2apk' | "pwa2apk" (default) or "bubblewrap". |
push_timeout | Optional[float] | None | Optional seconds for the write_bytes adb push of the built APK. When None the server default applies. |
Image.uv_install#
def uv_install(*packages: str) -> ImageInstall Python packages via uv add into the cua-server project.
Image.pip_install#
def pip_install(*packages: str) -> ImageInstall Python packages via pip.
Image.app_install#
def app_install(app_id: str) -> ImageInstall an app from the cua-sandbox-apps catalog.
Requires cua-sandbox-apps to be installed. The app's install
script for this image's OS is executed as a build layer.
Image.run#
def run(command: str) -> ImageRun a shell command during image build.
Image.env#
def env(**variables: str) -> ImageSet environment variables.
Image.copy#
def copy(src: str, dst: str) -> ImageCopy a file into the image.
Image.expose#
def expose(port: int) -> ImageExpose a port.
Image.to_dict#
def to_dict() -> Dict[str, Any]Serialize to a plain dict suitable for JSON.
Image.to_cloud_init#
def to_cloud_init() -> strGenerate a cloud-init user-data script from the image layers.
Image.local_support#
def local_support()Check whether this image can run locally on the current host.
Returns a cua_sandbox.runtime.compat.RuntimeSupport describing:
supported: runtime is available or auto-installable on this OShw_accel: hardware acceleration (HVF / KVM / Hyper-V) is availableruntime_installed: runtime binary found right now (no install needed)auto_installable: SDK can install the runtime automaticallyreason: human-readable explanation
In tests, use the bundled helper instead of checking manually:
from cua_sandbox.runtime.compat import skip_if_unsupported
async def test_something():
skip_if_unsupported(Image.macos())
async with Sandbox.ephemeral(Image.macos(), local=True) as sb:
...ImageInfo#
class ImageInfo
ImageInfo(
reference: str,
pinned_ref: str,
digest: str,
variant: str,
arch: Optional[str],
os: str,
emulated: bool,
)The image a sandbox runs, as resolved and pinned at create time.
reference is the reference as requested (normalised, e.g.
docker.io/library/python:3.12-slim), pinned_ref the
registry/repo@sha256:... that ran, variant one of rootfs,
containerdisk or lume, arch the architecture that
ran (amd64/arm64, when known), os the guest OS and
emulated whether it ran under emulation.
| Attribute | Type | Description |
|---|---|---|
reference | str | |
pinned_ref | str | |
digest | str | |
variant | str | |
arch | Optional[str] | |
os | str | |
emulated | bool |
Containers and registries#
Container#
class Container
Container(
image: str,
command: Optional[Sequence[str]] = None,
env: Optional[Mapping[str, str]] = None,
ports: Optional[Sequence[int]] = None,
name: Optional[str] = None,
)A sidecar container, reachable from the sandbox at its name.
name (a DNS label, not main) defaults from the image (redis
for redis:7-alpine); the sidecar reaches the sandbox at main.
command replaces the image's entrypoint. env holds plain values,
not secrets.
| Attribute | Type | Description |
|---|---|---|
image | str | |
command | Optional[Sequence[str]] | |
env | Optional[Mapping[str, str]] | |
ports | Optional[Sequence[int]] | |
name | Optional[str] |
Container.native#
def native() -> AnyRegistrySecret#
class RegistrySecret
RegistrySecret(
username: str,
password: str,
*,
registry: Optional[str] = None,
)Credentials for a private registry image.
RegistrySecret(username, password): explicit values (a token works as the password).RegistrySecret.from_env(): readCUA_REGISTRY_USERNAME/CUA_REGISTRY_PASSWORD(or the variables you name) at create time.RegistrySecret.aws_ecr(region=None): a private Amazon ECR image, with a login token from the AWS CLI.
registry scopes them to one host; by default they apply to the
image's registry. The password never appears in repr or logs.
| Attribute | Type | Description |
|---|---|---|
registry | Optional[str] | |
username | Optional[str] |
Example
from cua_sandbox import Image, RegistrySecret
print(Image.from_registry("python:3.12-slim").to_dict()["registry"])
print(repr(RegistrySecret("robot", "s3cr3t", registry="registry.example.com"))) # the password never showsRegistrySecret.from_env#
@classmethod
def from_env(
username_var: str = 'CUA_REGISTRY_USERNAME',
password_var: str = 'CUA_REGISTRY_PASSWORD',
*,
registry: Optional[str] = None,
) -> 'RegistrySecret'RegistrySecret.aws_ecr#
@classmethod
def aws_ecr(region: Optional[str] = None) -> 'RegistrySecret'RegistrySecret.native#
def native() -> AnyPools on this machine#
create_pool keeps sandboxes of one shape ready on this machine (the cua daemon holds them); Sandbox.create(pool=...) takes one in about 0.1 s.
Pools#
create_pool#
async def create_pool(
image: 'Image | str',
*,
name: Optional[str] = None,
min: int = 1,
max: Optional[int] = None,
idle: Seconds = None,
on: str = 'local',
kind: Optional[str] = None,
runtime: Optional[str] = None,
cpu: Optional[int] = None,
memory: 'str | int | None' = None,
memory_mb: Optional[int] = None,
env: Optional[dict[str, str]] = None,
services: Optional[dict[str, int]] = None,
) -> SandboxPoolCreates pool name of image sandboxes (None: a generated
name), or updates the pool of that name. kind, runtime, cpu,
memory, env and services mean what they mean in
Sandbox.create. Returns at once; its sandboxes start in the
background (see the module docs for min, max and idle).
SandboxPool#
class SandboxPool
SandboxPool(
name: str,
image: str,
location: str,
kind: str,
runtime: str,
cpus: int,
memory_mb: int,
min: int,
max: int,
idle: Optional[timedelta],
ready: int,
starting: int,
status: str,
)A pool (see cua_sandbox.sandbox_pool), with what it held when
read. Make one with create_pool.
| Attribute | Type | Description |
|---|---|---|
name | str | |
image | str | |
location | str | |
kind | str | |
runtime | str | |
cpus | int | |
memory_mb | int | |
min | int | |
max | int | |
idle | Optional[timedelta] | |
ready | int | |
starting | int | |
status | str |
SandboxPool.create#
async def create(name: Optional[str] = None, **kwargs: Any) -> 'Sandbox'A sandbox from this pool (Sandbox.create(pool=self, ...)).
SandboxPool.ephemeral#
def ephemeral(**kwargs: Any) -> 'AsyncIterator[Sandbox]'async with pool.ephemeral() as sb: a sandbox from this pool,
deleted on exit.
SandboxPool.refresh#
async def refresh() -> 'SandboxPool'This pool read again (what it holds now).
SandboxPool.delete#
async def delete() -> boolDeletes this pool and its ready sandboxes (sandboxes created from it stay). Whether it existed.
get_pool#
async def get_pool(name: str) -> Optional[SandboxPool]Pool name with what it holds now, or None when there is none.
list_pools#
async def list_pools() -> list[SandboxPool]Every pool, with what each holds now.
delete_pool#
async def delete_pool(name: str) -> boolDeletes pool name and its ready sandboxes (sandboxes created from
it stay). Whether it existed.
Services, tunnels and MCP#
Reach ports inside a sandbox: named services, tunnels, shareable URLs and MCP servers.
Named services#
ServiceHandle#
class ServiceHandleOne named service of a sandbox (sb.service("mcp")).
Requests carry your headers, url() is usable from this machine and
public_url() can be shared.
| Attribute | Type | Description |
|---|---|---|
name | str |
ServiceHandle.request#
async def request(
method: str,
path: str = '/',
*,
headers: Headers = None,
body: bytes | str | None = None,
json: Any = None,
timeout: float = 30.0,
) -> AnyOne HTTP request to path on the service; returns an
httpx.Response.
ServiceHandle.url#
async def url() -> strA URL for the service usable from this machine with no credentials (no trailing slash): the published loopback port.
ServiceHandle.public_url#
async def public_url(ttl: float = 3600, *, label: Optional[str] = None) -> 'PublicUrl'A shareable URL that stops working after ttl seconds (60 s to
24 h). See cua_sandbox.Sandbox.public_url.
Services#
class ServicesRequest a named service exposed by the active sandbox connection.
Services.request#
async def request(
name: str,
*,
method: str,
path: str,
json: Any = None,
headers: dict[str, str] | None = None,
) -> AnySend a request to a named service on this sandbox.
Prefer sb.service(name).request(...).
Tunnels#
Tunnel#
class TunnelPort-forwarding interface: exposes sandbox ports on the host.
Tunnel.forward#
def forward(*ports: int | str) -> _TunnelContextForward one or more sandbox ports (or abstract sockets) to the host.
ports may be:
int: TCP port inside the sandbox (e.g.8080)str: Android abstract socket name (e.g."chrome_devtools_remote")
Returns a context manager (or awaitable) that yields:
- a single
TunnelInfowhen one target is given - a
dict[sandbox_port, TunnelInfo]when multiple targets are given
Example: Chrome DevTools over ADB:
async with sb.tunnel.forward("chrome_devtools_remote") as t:
# http://localhost:<random>/json lists CDP targets
print(t.url)TunnelInfo#
class TunnelInfoA single forwarded port.
| Attribute | Type | Description |
|---|---|---|
host | str | |
port | int | |
sandbox_port | int | |
url | str |
TunnelInfo.close#
async def close() -> NoneClose this tunnel (no-op if already closed or inside a context manager).
MCP#
mcp_config#
async def mcp_config(
service: Callable[[], Awaitable[Any]],
path: str = '/mcp',
) -> Dict[str, Any]{"url": ..., "headers": {...}} of the MCP endpoint at path.
connect#
async def connect(config: Dict[str, Any]) -> AsyncIterator[Any]An initialized official-SDK client for config (url, headers).
With mcp 2.x this is an mcp.Client (protocol negotiated with
server/discover, falling back to initialize); with 1.x an
initialized mcp.ClientSession.
open_mcp#
async def open_mcp(
service: Callable[[], Awaitable[Any]],
path: str = '/mcp',
) -> AsyncIterator[Any]connect(await mcp_config(service, path)).