Cua Volume

One versioned drive per account, shared by every Space and agent: agent memory and outputs that outlive any Space, with default access rules, grants you approve, and an audit log. Also how to send a one-off file into a Space.

Cua Volume keeps what outlives a Space: a persistent agent's memory, its outputs, and files the user shares with every Space. A Space's own disk goes away when the Space is deleted; the drive does not. To copy a one-off file into one Space instead, see Send a file into a Space.

Layout#

FolderWhat it holds
public/Shared reference: docs, datasets, house rules, skills
agents/<agent>/One named agent's home: memory, outputs/, inbox/
spaces/<space>/Per-Space scratch and outputs (local:work is spaces/local-work/)

Every write is a new version. Deletes leave a marker, so history and restore still reach the old content.

Who may reach what#

Principalpublic/agents/<self>/agents/<other>/spaces/<this>/spaces/<other>/
You (app, CLI, agents on your computer)rwrwrwrwrw
Agent A in Space Srrwnonerwnone
Space S, no agentrnonenonerwnone

Anything wider is a grant: a principal, a folder or file, r or rw, and an optional expiry. Only you grant, and every grant asks for Touch ID, your login password or the Keyvault passphrase first. An agent that needs more files a request; you approve it (with presence) or deny it.

The daemon checks every read, write and listing before it touches storage. An agent in a Space never holds keys to the drive.

From a terminal#

cua volume ls agents/
cua volume put public/rules.md ./rules.md
cua volume cat agents/ada/memory/MEMORY.md
cua volume history agents/ada/memory/MEMORY.md
cua volume restore agents/ada/memory/MEMORY.md 0001790683200000-1a2b3c4d
cua volume ls --as agent:ada --space local:work

--as shows the drive exactly as that agent sees it. It only narrows.

cua volume grant agent:researcher agents/writer/outputs/
cua volume grant space:local-build public/datasets/ --rw --for 2h
cua volume grants
cua volume requests
cua volume approve 5f2c9a0b1d3e4f67
cua volume revoke 5f2c9a0b1d3e4f67
cua volume audit

See the cua volume reference.

From the SDK#

The drive methods live on Spaces; a DriveView takes an agent's view. They ask the Cua Spaces daemon, which runs the drive; an SDK runtime without Cua Spaces raises HostCapabilityMissing.

import cua
 
spaces = cua.connect().spaces()   # the Cua Spaces daemon
await spaces.volume_write("public/rules.md", b"be kind", None, False, None)
ada = cua.DriveView(as_agent="ada", in_space="local:work")
await spaces.volume_write("agents/ada/memory/MEMORY.md", b"likes tea", None, False, ada)
try:
    await spaces.volume_read("agents/bob/notes.md", None, ada)
except cua.CuaError.PermissionDenied:
    print("ada may not read bob's home")
print([e.path for e in (await spaces.volume_ls("agents/", ada)).entries])
# expect: ['agents/ada/']

Every tool is also on the Spaces MCP server (volume_ls, volume_read, volume_write, ...): see the volume tools reference and the SDK reference.

Secrets stay in the Keyvault#

Writes under agents/ are scanned for API keys, tokens and private keys. A hit is refused with secret_detected, naming the kind and the line, never the value, and the audit log records it. Agent homes never sync harness credentials (.env, auth.json, credentials/, vault/). Keep secrets in the Keyvault.

Audit#

cua volume audit lists grants, revocations, requests, refusals, blocked secrets, syncs, leases and every agent read and write, newest first. Each line's hash covers the previous one, so an edited or truncated log reports that it does not verify.

Show it in Finder#

The drive can appear as a volume named Cua Volume, so any app opens its files in place. It is off until you turn it on in Cua (Settings > Storage) or from a terminal:

cua volume mount
cua volume status
cua volume unmount
SystemHow it mountsWhere
macOSA local NFS server inside the daemon, mounted with the system's own client. No kernel extension and no approval step~/Cua Volume, listed in Finder under Locations
LinuxFUSE~/cua-volume

The setting survives restarts: the daemon mounts the volume when it starts and unmounts it (after pending uploads land) when it stops.

What happens underneath:

  • Large files stream. Opening a file reads only the bytes an app asks for, a block at a time, from a local block cache (~/.cua/volume/cache, 10 GiB by default, least recently used first). When an app reads sequentially, as a player or an editor does, the drive fetches ahead of it, so a 4K video starts in well under a second instead of after a full download.
  • Writes upload on their own. A file uploads when the app closes it, or after 1.5 seconds without writes. Large files go up in parts. The access rules, the audit log and the secret scanner apply exactly as they do to cua volume put.
  • Finder's own files stay local. .DS_Store and ._* files are kept on this Mac and never upload.

How fast it is#

Measured on an Apple M5 Max (macOS 26.5) through the mounted volume, each number from a cold start (the block cache cleared and the volume remounted). The videos are 4K H.264 at 150 Mb/s with the index at the end of the file, as cameras write it: 1.04 GB (56 s) and 10.4 GB (9 min 20 s). "Cloud" is a local MinIO behind a proxy that adds 25 ms each way and caps each connection at 50 MB/s. "Download first" is a full download with 8 parallel ranged requests, then the same step on the local copy.

StoreFileProbe (ffprobe)Frame at 50%Decode 10 s from the middleDownload first, then probe
This Mac (fs)1 GB0.04 s0.11 s0.49 s0.40 s
This Mac (fs)10 GB0.10 s0.21 s1.09 s5.7 s
MinIO1 GB0.10 s0.20 s1.17 s0.35 s
MinIO10 GB0.08 s0.18 s0.72 s7.5 s
Cloud1 GB0.49 s0.50 s1.17 s3.3 s
Cloud10 GB0.35 s0.34 s1.14 s32.6 s
StoreFileSequential readRandom 4 KiB read, p50 / p95Cache hits during the 10 s decode
This Mac (fs)10 GB1,324 MB/s1.1 / 1.8 ms(no cache: already local)
MinIO10 GB737 MB/s3.1 / 4.0 ms85%
Cloud10 GB212 MB/s76 / 92 ms42%

A folder of 10,000 files lists through the volume in 0.18 s (fs) and 0.47 s (MinIO) the first time, and in about 1 ms after that.

The benchmark is libs/cua/crates/cua-volume/bench/drive_bench.py; it prints the full set of numbers, including 1 GB rows and sync latency.

Inside a Space#

Every Space mounts the same volume, so a program in the guest reads and writes it like any folder. The guest sees only that Space's view (the Space's row in the table above, or its agent's), and it never holds keys: your computer serves the view over the Space's own connection and applies the access rules, the audit log and the secret scanner there.

GuestHow it mountsWhere
macOSThe system's own NFS client~/Cua Volume
LinuxFUSE (the image's root volume helper mounts it)/volume, else ~/Cua Volume
WindowsComing soon. Until then, agents in a Windows Space use the volume_* tools

The mount goes away when the Space detaches the volume, when its session is revoked and when the guest's cua-spacesd stops. cua-spacesd doctor checks the mount end to end (volume.mount).

When the guest's cua-spacesd restarts#

An update, a crash or a reboot restarts the guest's cua-spacesd, which holds the volume's connection and the network-sharing (hotspot) connection. You do not need to reconnect the Space:

  • On a clean stop (SIGTERM) the daemon unmounts the volume before it exits, so no dead mount is left behind.
  • When it starts, the daemon unmounts anything a previous daemon left at the mount path (a crash, a force-quit) before it does anything else.
  • Your computer notices the connection closed, or that the new daemon reports the volume detached, and mounts the volume again, then restarts the hotspot if one was on. It retries with a growing wait (2 s, then 4 s, up to a minute) while the daemon is still coming up, and keeps a persistent agent's view on the volume.
  • A mount whose server is gone never hangs a program for long: the NFS mount is soft, interruptible and times out in seconds, and the system marks it dead and unmounts it (deadtimeout).

Named volumes: share a folder between Spaces#

A named volume is the folder volumes/<name>/ of your Cua Volume. Any number of Spaces, of any kind (containers, microVMs and VMs on Linux, and macOS VMs), can mount it at a path of their choosing, read-write or read-only, and the files outlive every one of them. A Space sees only the volumes it mounted; the access rules, versioning and audit log of the drive apply to them like to any other folder.

cua volume named create shared
cua spaces create --volume shared:/mnt/shared                 # read-write
cua spaces create --volume shared:/mnt/shared:ro              # read-only
cua spaces create --volume shared/inputs:/data:ro             # one folder of it
cua volume named ls                                           # and which Spaces mount each
cua volume named rm shared                                    # refused while a Space mounts it; --force
await spaces.create_volume("shared")
space = (await spaces.create(cua.SpaceCreateOptions(
    volumes=[cua.VolumeMount(name="shared", path="/mnt/shared", read_only=True)],
))).space

The files of a volume are the drive's files under volumes/<name>/ (cua volume ls volumes/shared/, volume_read, volume_write).

  • Linux guests bind-mount the folder at the path, read-only when asked (a write fails with EROFS). The path is created when it is missing; the usual data roots (/mnt, /data, /srv, /workspace) and folders the Space's user owns are accepted, system directories are not.
  • macOS guests get a symbolic link at the path to the folder under ~/Cua Volume. A link cannot be read-only; a read-only mount is enforced by the drive instead (writes fail with a permission error).
  • The mounts are part of the Space's record. They come back when the guest's cua-spacesd restarts or the Space stops and starts, and a fork mounts the same volumes.
  • A create fails, and leaves nothing behind, when a volume does not exist, when two mounts overlap, or when the guest cannot mount (a container without /dev/fuse, an older cua-spacesd without volume.binds).

Send a file into a Space#

To copy a one-off file or folder into one Space, without keeping it on the volume, use send_file. It puts the file in the Space user's ~/Downloads (or a subfolder). Every file is verified by SHA-256 before it is published, and a transfer that does not land is an error.

  • For a folder, .gitignore, .ignore and a root .dockerignore are honored (respect_ignore_files).
  • Nothing already in the Space is replaced by default. A file of the same name is kept and the new copy lands as name (1).ext (conflict: "rename"). Pass conflict: "skip" to keep only the existing file, or conflict: "overwrite" to replace it.

From the examples in other languages:

const report = await this.space.sendFile(localPath, {
  targetDirectory: subdir,
  respectIgnoreFiles: true,
  conflict: "rename",
})
if (!report.verified) throw new Error(`send_file of ${localPath} was not verified`)

Python: await space.send_file(path, cua.SpaceSendFileOptions(target_directory="inbox")). Agents use the MCP tool send_file (what your agent can do). The options are in the send_file reference.

Sync across computers#

Two computers using the same bucket see each other's changes within seconds. Each change is also written to a small change log in the bucket itself (.cua-feed/), and every computer checks it with a single listing request, twice a second while things change, backing off to every 5 seconds when idle. No Cua service is involved; it works with your own S3, R2 or MinIO bucket.

Measured with two cua homes on one MinIO bucket (20 rounds each): a change made on one reaches the other's daemon in 0.50 s at the median and 0.50 s at p95 for a 1 KiB file, and shows in the other's mounted volume 2 ms to 5 ms later. A 64 MiB file shows 0.32 s after its upload finishes (p95 0.34 s). While nothing changes, the check backs off, so the first change after a quiet period can take up to 5 seconds.

When two computers change the same file, the write that reaches the bucket last becomes the current version, and the other one is kept next to it as name (conflict from <machine> <date>).ext. Both stay in the file's history. Nothing is dropped silently.

cua volume status --json

Keep the change log small with a bucket lifecycle rule that expires noncurrent versions under .cua-feed/ after a day. Each computer already deletes feed entries older than a day.

In the Cua Spaces app#

First run. The Cua Volume page asks where your files live: This Mac (the default, with the folder it uses), your own S3 bucket (AWS S3, R2, MinIO or any S3-compatible store), or later in Settings. Its one checkbox, Add Cua Volume to Finder (on Linux, Mount Cua Volume), is off by default; ticking it mounts the volume when you continue. Windows has no mount yet, so the page does not show there.

The first run's Cua Volume page

For your own bucket the page offers a prompt to give your coding agent: it creates a private, versioned bucket and a user limited to it with the AWS CLI, then connects it with cua volume config set and cua volume config set-keys. The app notices the bucket as soon as it is configured and shows "Connected, versioning on". Enter details manually shows the endpoint, bucket, region and key fields with Test connection instead.

Your S3 bucket: the prompt for your agent

Settings, Storage. Where the files live, the volume, and the block cache. Test connection checks the endpoint, the keys and bucket versioning without saving anything. Keys go to the credential store, never to config.json, and Save switches the running volume without a restart.

Settings, Storage

Menu bar. The menu shows the volume's sync state next to the number of Spaces (Synced, Syncing, Offline) and any conflicts, which open the Volume page. Nothing shows while the files stay on this Mac with no other computer.

Volume page. Status, not a file browser: Finder is. Open in Finder shows the volume, mounting it first when it is not mounted. Computers lists every computer on the bucket, this one with its pending uploads and last sync. Conflicts lists files two computers wrote at once: Open shows the other copy in Finder, Resolve clears the entry and keeps both files.

The Volume page with two computers and a conflict

What a Space sees#

With Secure Spaces (always on), a Space sees only its own folder:

  • A Space's folder carries the Space's own id, which never changes: local:work is spaces/local-work--0a1b2c3d/. If you delete a Space and later make another with the same name, the new one starts with an empty folder and cannot see the old one, which stays in your Cua Volume until you delete it. A folder from before ids existed (spaces/local-work/) is renamed the next time its Space mounts the volume, and that Space keeps the read-only access to public/ it had before.
  • public/ is read only through a grant: for one Space (space:<id>, which also covers the agent working in it) or for every Space of a tree (tree:<root>, the first Space and the Spaces it started). Only you grant, and every grant asks for Touch ID, your login password or the Keyvault passphrase first. Revoking a grant closes access at once.
cua volume grant space:local:work public/                     # one Space, read-only
cua volume grant tree:local:lead public/datasets/ --for 2h    # a Space and its children
  • A Space's Volume setting is Own folder (the default) or Off (cua spaces set <space> --volume off). Off takes the Space's folder away; the Space gets no /volume mount unless it also mounts a named volume. It applies to the running Space at once.
  • A named volume mounted with --volume NAME:PATH is read-only unless you add :rw. A mount saved before this keeps the mode it was saved with, and the daemon logs a warning each time it is mounted read-write.

Without the experiment, public/ is read by every Space and a mount without :ro is read-write, as before.

Storage#

BackendWhereSet up
fs (default)~/.cua/volume/data on this computerNothing
s3Any S3-compatible bucket with versioning on (AWS S3, R2, MinIO)cua volume config set and cua volume config set-keys
cua volume config show
cua volume config set --backend s3 --endpoint http://127.0.0.1:9000 --bucket cua-volume --path-style
cua volume config set-keys

set-keys reads the access key id and the secret from stdin and saves them in the credential store; they never go in ~/.cua/volume/config.json. Restart the daemon after changing the backend from the terminal. Settings > Storage in Cua tests the bucket first (reachable, keys allowed to write, versioning on) and switches the running drive without a restart.

Limits#

LimitValue
One read or write through a tool8 MiB
One file through the mounted volumeNo limit (streamed; large files upload in 16 MiB parts)
Block cache10 GiB by default, at least 256 MiB
Path length1024 bytes
Agent names1 to 63 of a-z, 0-9, ., _, -
Requests waiting per agent8
Cloud keys15 to 60 minutes, at most 12 folders
MinIOa file and a folder cannot share a name (a and a/b)