Create a Space
Create a Space on a computer you own from the app, the CLI or code: any image, progress, cancel, GPUs, adding an existing computer by address, then list, delete or remove it.
A Space is one desktop on one of your computers. Ids are the sandbox refs
cua sb ls prints (local:<name>, aws:<name>, direct:<host:port>,
relay:<machine-id>); a bare name works when it matches one Space. To connect a
whole computer first, see Add a computer.
Create a Space in the app#
- Press New Space in Cua Spaces.
- Walk through Choose a system, Resources and Options. Summary shows what you picked.
- Press Create Space. The Space appears in your list with a progress bar and is ready when the bar finishes.
Advanced in Choose a system picks the kind (container, virtual machine or microVM) and the runtime. See Space kinds.
If the create fails, see Troubleshoot.
Add an existing computer by address#
Choose Connect by address in New Space, or register a computer from code. This example starts the canonical Linux image with a token:
TOKEN=$(openssl rand -hex 24)
docker run -d --name my-space -e CUA_ENV_TOKEN=$TOKEN -p 127.0.0.1:3211:3211 \
ghcr.io/trycua/linux:24.04Register it and run a command in it (use http://127.0.0.1:3211 and your
$TOKEN):
import asyncio
import cua
async def main():
spaces = cua.embedded().spaces() # or cua.connect().spaces()
info = await spaces.add("http://10.0.0.5:3211", "TOKEN", "lab")
space = await spaces.space(info.id)
out = await space.bash("uname -a", None)
print(info.id, out.stdout)
asyncio.run(main())The program prints the Space id (direct:127.0.0.1:3211) and the guest's
uname -a. The Space is now in the shared registry, so the app and the CLI see
it too:
cua --version
cua auth status
cua spaces lsClean up the container with docker rm -f my-space.
Create a Space from code#
created = await spaces.create(cua.SpaceCreateOptions(cpus=2, memory_mb=4096))
space = await spaces.space(created.space.id)Runs ghcr.io/trycua/linux:24.04 (or image) with a fresh env token. on,
kind and runtime work as for sandboxes
(see Space kinds);
without on, the Space runs where cua config get default.on says (local
unless you changed it; the Spaces app sets it in Settings, Default location).
cua runtime doctor shows what this host has.
reuse=True returns a registered, reachable Space in the same location first
(created.reused says so). With wait=False, created.pending_id names a
Space that is not ready yet.
A Space from any image#
create takes image, command, env and services. An image that serves
MCP becomes a Space of that service, with no cua-spacesd:
created = await spaces.create(cua.SpaceCreateOptions(
image="<your-mcp-server-image>",
command=["python", "-m", "acme_mcp", "--port", "8765"],
services={"mcp": 8765},
))
space = await spaces.space(created.space.id)
tools = await space.list_tools("mcp")
result = await space.call_tool("add", '{"a": 2, "b": 3}', "mcp", None)| Works on every Space | Needs cua-spacesd in the image |
|---|---|
Lifecycle, declared services, tunnels, public URLs, list_tools / call_tool with service | bash, files, streaming, presence, teleport, agent runs, the driver tool registry |
Spacesd features on an image without it fail with CapabilityMissing. The
canonical images and computers set up with cua host setup run cua-spacesd, and
so does any OCI image you create on this computer as a container or microVM
(alpine:3.20, node:20): a static cua-spacesd is put into it and runs
beside the image's own command.
In the cloud, command and env work on container and VM images alike.
Keep secrets out of env.
Progress#

The app shows a progress bar, and under it while the image downloads
4.2 of 22.1 GB · 85 MB/s · about 4 min. cua spaces create prints the
same on one updating line. In code, pass a listener:
class Show(cua.SpaceCreateListener):
def on_progress(self, p):
print(p.space, p.phase, p.bytes_done, p.bytes_total, p.bytes_per_second)
created = await spaces.create_with_progress(cua.SpaceCreateOptions(image="macos"), Show())| Field | Value |
|---|---|
phase | preparing, pulling, creating, booting, waiting_for_services, connecting |
fraction | 0 to 1, when the step has one |
bytes_done, bytes_total, bytes_per_second | while an image downloads |
space | the id the Space will have (local:<name>) |
macOS images report bytes with a Lume whose pull API reports them
(downloadedBytes); an older Lume reports only a percent.
Cancel a create#

- App: Cancel in the toolbar of a Space that is being created.
- CLI: Ctrl-C during
cua spaces createorcua sb create(exit code 130), or from any terminal:
cua spaces cancel local:space-1a2b3c- SDK:
cancel_createtakes the create'screate_id, the id the Space will have (spacein every progress report) or its name, and returns once the cleanup is done. The create fails withCuaError.Cancelled.
outcome = await spaces.cancel_create("local:space-1a2b3c")
print(outcome.state, outcome.message) # cancelled, not_creating or already_createdSandboxes: await sandboxes.cancel_create(name) returns what was removed, or
None. A cancel works across processes and after a daemon restart.
| Removed | Kept |
|---|---|
| The VM or container and its disks, the cloud claim, the relay computer, the registry entry, the create's journal | Anything that existed before the create |
| Partial downloads | Finished image layers in the engine's cache (Docker's; Lume's when its cache is on), so the next create resumes |
A Lume without POST /lume/pull/cancel cannot stop a pull: the create still
cancels, and the image finishes downloading in the background as a reusable
base.
GPU acceleration#
cua spaces gpus
cua spaces create macos --gpu paravirtual--gpu alone (or gpu="auto") picks the runtime's own option; the SDK
takes SpaceCreateOptions(gpu=...) and SandboxCreateOptions(gpu=...), and
spaces.gpu_support(on) lists what exists here.
| Runtime | Option | Where |
|---|---|---|
| Lume (macOS VMs) | paravirtual, GPU acceleration (experimental) | Apple silicon |
| QEMU | virgl | Linux hosts whose QEMU has virglrenderer, egl-headless and a render node; none on macOS |
| Containers | nvidia | Linux with the nvidia runtime, runc (not gVisor); none on macOS (Docker runs Linux in a VM without GPU access) |
| Modal | T4, L4, A10G, A100, H100 | |
| E2B, Daytona | none |

In the app it is the GPU checkbox on the Resources step of New Space. For
paravirtual, cua applies the unrestricted paravirtualized GPU feature level
before each start of the VM and removes it when the last GPU VM is deleted,
if cua set it. Metal apps in the guest may still need the per-workload shim:
Unlock Metal for a workload.
List, delete, remove#
cua spaces lsLists registered Spaces and, when you are signed in, your computers on the
relay (relay:<machine-id>, no add needed). Register or forget one from a
shell:
cua spaces add 10.0.0.5:3211 --token "$CUA_ENV_TOKEN" --name studio
cua spaces rm studioA Tailscale address works here too (its IP or
MagicDNS name as the host:port): cua spaces add reaches it like any other
address and the Space registers as direct:<addr>. That only attaches one
already-running Space. To host Spaces for on="host:<name>", set the computer up
with cua host setup.
spaces.delete(id) deletes a Space's sandbox and forgets it (a Space added by
address is only forgotten). spaces.remove(id) forgets a Space without touching
it.
In TypeScript the methods are create, delete_, add and remove, with
options from @trycua/cua/spaces. Agents use the MCP tools create_space,
delete_space, add_space and remove_space
(What your agent can do).