> ## Documentation Index
> Fetch the complete documentation index at: https://docs.declaw.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Volumes

> Upload a tarball once and attach it to one or many Declaw sandboxes at create time — TypeScript SDK.

```typescript theme={null}
import { Sandbox, Volumes } from '@declaw/sdk';
import type { VolumeInfo, VolumeAttachment } from '@declaw/sdk';
```

A **volume** is a tenant-owned blob (gzip-compressed tar archive) that lives in Declaw's object store. You upload a volume once with `Volumes.create(...)` and attach it to any number of sandboxes at create time via `Sandbox.create({ volumes: [...] })`. On boot, Declaw streams the blob from object storage and materializes its regular-file entries under the attachment's `mountPath` before the first command runs.

## How volumes work

* **Format:** only gzip-compressed tar archives (`application/gzip`). Symlinks, hardlinks, device nodes, and entries containing `..` are dropped on the server.
* **Size:** the upload body is capped at 4 GiB; a file-granular volume has a flat **64 GiB** capacity cap.
* **Semantics:** read-at-boot. A volume is materialized into each sandbox's overlay filesystem when it attaches. Writes inside the sandbox are private to that sandbox and never flow back to the volume.
* **Ownership:** a volume is strictly owner-scoped. You can attach only your own volumes.

## `Volumes.create()`

Upload a tar.gz and register it. The body is a `Uint8Array` or `ArrayBuffer`; pass a streaming body from disk by reading with `fs.readFile`, or build it in-memory (see the cookbook for a zero-dep tar writer).

```typescript theme={null}
import { readFile } from 'node:fs/promises';
import { Volumes } from '@declaw/sdk';

const bytes = await readFile('dataset.tar.gz');
const vol = await Volumes.create('training-set-v1', bytes);
console.log(vol.volumeId, vol.sizeBytes);
```

**Signature**

```typescript theme={null}
Volumes.create(
  name: string,
  data: Uint8Array | ArrayBuffer,
  opts?: VolumeCreateOpts,
): Promise<VolumeInfo>
```

<ParamField body="name" type="string" required>
  Human-readable name. Not used for addressing — the server returns a stable `volumeId`.
</ParamField>

<ParamField body="data" type="Uint8Array | ArrayBuffer" required>
  The raw tar.gz bytes to upload.
</ParamField>

<ParamField body="opts.contentType" type="string" default="'application/gzip'">
  Content-Type header sent with the upload. Leave as the default.
</ParamField>

<ParamField body="opts.apiKey" type="string">
  Override the API key from environment.
</ParamField>

<ParamField body="opts.domain" type="string">
  Override the API domain (e.g. `api.declaw.ai`).
</ParamField>

<ParamField body="opts.requestTimeout" type="number">
  Per-request timeout in milliseconds. Raise this for multi-GiB uploads.
</ParamField>

## `Volumes.list()`

List all volumes owned by the caller, newest first.

```typescript theme={null}
for (const v of await Volumes.list()) {
  console.log(v.volumeId, v.name, v.sizeBytes);
}
```

## `Volumes.get()`

Fetch metadata for a single volume.

```typescript theme={null}
const vol = await Volumes.get('vol-abc123');
```

Throws `NotFoundError` if the volume does not exist or is owned by a different tenant.

## `Volumes.delete()`

Delete the blob and the metadata row.

```typescript theme={null}
await Volumes.delete('vol-abc123');
```

## Attaching to a sandbox

Pass `volumes: [...]` to `Sandbox.create`:

```typescript theme={null}
import { Sandbox, Volumes } from '@declaw/sdk';
import type { VolumeAttachment } from '@declaw/sdk';

const vol = await Volumes.create('dataset', await readFile('dataset.tar.gz'));

const sbx = await Sandbox.create({
  template: 'python',
  timeout: 600,
  volumes: [{ volumeId: vol.volumeId, mountPath: '/data' }],
});

const result = await sbx.commands.run('ls -la /data');
console.log(result.stdout);
```

<ParamField body="volumes" type="VolumeAttachment[]">
  One or more attachments. Each is `{ volumeId: string, mountPath: string }`. `mountPath` must be an absolute path and must not target a system directory (`/`, `/etc`, `/usr`, `/proc`, `/sys`, `/dev`, `/bin`, `/sbin`, `/lib`, `/lib64`, `/var`, `/run`, `/boot`).
</ParamField>

The same `volumeId` can appear in many sandbox-create calls in parallel; each sandbox gets its own materialized copy on its overlay.

## File-granular volumes (live mounts)

The volumes above are **copy-mode**: a tar.gz hydrated into the sandbox at boot, with writes private to each sandbox. A **file-granular** volume is different — you can edit its files directly from the SDK (no sandbox), and **live-mount** it into a sandbox so reads *and* writes go straight to the shared volume.

|                          | Copy (`Volumes.create`)  | File-granular (`Volumes.empty` / `ingest`) |
| ------------------------ | ------------------------ | ------------------------------------------ |
| Created from             | a tar.gz blob            | empty, or a tar.gz (`ingest`)              |
| Edit without a sandbox   | no                       | yes — the `files` API                      |
| Attach mode              | `copy` (hydrate at boot) | `copy`, `mount` (rw), or `mount-ro`        |
| Sandbox writes flow back | no (private copy)        | yes (live mount)                           |

### Create a file-granular volume

```typescript theme={null}
const vol = await Volumes.empty('scratch');               // empty
const vol2 = await Volumes.ingest('seed', tarGzBytes);    // or from a tar.gz (Uint8Array)
console.log(vol.backend);   // "juicefs" / "local" (not "tarball")
```

### Edit files without a sandbox — `Volumes.files()`

```typescript theme={null}
const files = Volumes.files(vol.volumeId);
await files.write('/config/app.json', new TextEncoder().encode('{"k":"v"}')); // parent dirs auto-created
await files.mkdir('/data');
console.log(new TextDecoder().decode(await files.read('/config/app.json')));
for (const e of await files.list('/')) console.log(e.path, e.isDir, e.size);
await files.rename('/config/app.json', '/config/app.prod.json');
await files.remove('/data', { recursive: true });
```

`files.info(path)` returns a `version` token; pass it to `write(path, data, { ifVersion })` for an optimistic compare-and-set write — a `ConflictError` (409) means the file changed underneath you.

### Live-mount into a sandbox

```typescript theme={null}
const files = Volumes.files(vol.volumeId);
await files.write('/greeting.txt', new TextEncoder().encode('hello from the files API'));

const sbx = await Sandbox.create({
  template: 'base',
  volumes: [{ volumeId: vol.volumeId, mountPath: '/data', mode: 'mount' }],
});
// The sandbox reads the files-API write over a live NFS mount...
console.log((await sbx.commands.run('cat /data/greeting.txt')).stdout);
// ...and its writes are visible back through the files API immediately:
await sbx.commands.run("echo 'from the sandbox' > /data/out.txt");
console.log(new TextDecoder().decode(await files.read('/out.txt')));
```

Use `mode: 'mount-ro'` for a read-only mount — guest writes are rejected with a read-only-filesystem error. Live mounts require a file-granular volume; copy-mode volumes can only be attached with `mode: 'copy'`.

### Mount a sub-path

Mount just part of a volume with `subpath` (live-mount only — the server rejects `subpath` on a `copy` attachment):

```typescript theme={null}
const sbx = await Sandbox.create({
  template: 'base',
  volumes: [{
    volumeId: vol.volumeId,
    mountPath: '/data',
    mode: 'mount',
    subpath: 'datasets/train',   // mounts <volume>/datasets/train at /data
  }],
});
```

## Snapshot a sandbox's files into a volume

Capture filesystem state from a running sandbox into a **new** volume — the source is never modified:

```typescript theme={null}
// Any absolute in-sandbox path -> new volume
const snap = await Volumes.snapshot(sbx.sandboxId, '/workspace/out', 'run-42');

// An already-attached volume's mount path -> new volume
const checkpoint = await Volumes.commit(sbx.sandboxId, src.volumeId, 'checkpoint');
```

`snapshot` captures *any* in-sandbox path; `commit` captures the mount path of a volume already attached to that sandbox. Both return a new `VolumeInfo`; the `name` arg is optional. Synthetic paths (`/proc`, `/sys`, `/dev`) are rejected.

## Advisory locks

Coordinate writers to a shared (live-mounted) volume with advisory **leases** over a `(volume, path)` pair. `acquire` returns a token you must present to `renew` / `release`:

```typescript theme={null}
const locks = Volumes.locks(vol.volumeId);

const lease = await locks.acquire('/data/model.bin', 60);   // ConflictError (409) if already held
await locks.renew('/data/model.bin', lease.token, 60);
console.log(await locks.status('/data/model.bin'));         // { held: true, expiresInMs: ... }
await locks.release('/data/model.bin', lease.token);        // true if released
```

Locks are **advisory** — they coordinate cooperating writers; they don't block I/O from code that ignores them.

## `VolumeInfo` shape

```typescript theme={null}
interface VolumeInfo {
  volumeId: string;      // "vol-..."
  ownerId: string;
  name: string;          // human-readable, supplied at create
  blobKey: string;       // object-store path, for reference
  sizeBytes: number;
  contentType: string;   // "application/gzip"
  metadata: Record<string, string>;
  createdAt: string;     // ISO-8601
}
```

## Errors

| Situation                                                     | Error class            | HTTP |
| ------------------------------------------------------------- | ---------------------- | ---- |
| Volume not found or not owned by caller                       | `NotFoundError`        | 404  |
| `mountPath` is a system directory or relative                 | `InvalidArgumentError` | 400  |
| Referenced `volumeId` doesn't belong to caller at attach time | `AuthenticationError`  | 403  |
| Upload body exceeds 4 GiB                                     | `InvalidArgumentError` | 413  |

## See also

* [Cookbook → Upload and attach a volume](/cookbook/volumes/upload-and-attach)
* [Cookbook → Share a volume across sandboxes](/cookbook/volumes/share-across-sandboxes)
* [Cookbook → List and delete volumes](/cookbook/volumes/list-and-delete)
