# Drives: versioned files shared between runs

A drive is a named file tree of your account. Every change makes a new
**version**; versions never change afterwards, so a run that mounted a version
can always be reproduced. Runs mount drives at a path, read-only or
read-write.

## Create, write, read

```sh
curl https://shotcup.dev/api/v1/drives -H "Authorization: Bearer $SHOTCUP_API_KEY" \
  -H "Content-Type: application/json" -d '{"name": "prices"}'

curl -X PUT https://shotcup.dev/api/v1/drives/prices/files -H "Authorization: Bearer $SHOTCUP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"files": [{"path": "/eur.json", "content": "[1.08, 1.09]"}], "remove": ["/old.json"]}'
```

A write takes files like a run does (text, `encoding: "base64"`, an upload or
an `https` URL) and paths to remove (a directory removes what is under it),
and answers with the new version. Writing the files a version already has
makes no version (`created: false`). `GET /api/v1/drives/prices/files` lists a
version's files (the head unless `?version=` names one; `?prefix=/dir`
narrows it) and `?path=/eur.json` reads one file.

Every version has a `treeSha256`: the hash of its paths, file hashes, sizes
and modes, so equal trees have equal hashes, in any drive.

## Mount drives into runs

```json
{
  "code": "import fs from \"fs\";\nconst eur = JSON.parse(fs.readFileSync(\"/data/eur.json\", \"utf8\"));\nfs.writeFileSync(\"/results/sum.json\", JSON.stringify(eur.reduce((a, b) => a + b, 0)));",
  "mounts": [
    { "drive": "prices", "path": "/data" },
    { "drive": "results", "path": "/results", "mode": "rw" }
  ]
}
```

- `mode: "ro"` (the default): every write under the mount point fails with
  an `EROFS` error the script can catch; the drive never changes.
- `mode: "rw"`: when the run succeeds, what it changed under the mount point
  becomes a new version (removed files included). A run that fails keeps
  nothing. The run's `mounts` say which version it mounted (`baseVersion`),
  which version its changes became (`version`) and whether that is now the
  drive's head.
- `version` pins a version id; without it a mount uses the head when the run
  starts.

A saved sandbox takes `mounts` when it is created and mounts them into every
run; a run's own mount at the same path replaces it. Mount points are
absolute, never `/`, not inside one another, and no request file may lie
under one.

## Concurrent writers and merges

Runs that mount the same drive read-write at the same time never overwrite
each other. The first to finish moves the head; the others become **sibling**
versions of it, and their run's mount names the paths they changed
differently from the head (`conflictPaths`). Nothing is lost: reconcile the
open siblings when you choose to.

`POST /api/v1/drives/:name/merge` merges the open siblings (or the
`versions` you name) with the head against their common ancestor:

- without `script`: a three-way merge. A path one side changed takes that
  side; sides that changed it the same way agree. Paths the sides changed
  differently are conflicts: `strategy: "fail"` (the default) answers 409
  with them and stores nothing, `"head"` keeps the head's side, `"newest"`
  the most recently written side.
- with `script` (`{ "code": ... }` and the options of a run): a run that
  sees the common ancestor at `/merge/base`, the head at `/merge/head`, each
  sibling at `/merge/versions/1`, `/2`, ... (read-only), the conflicts in
  `/merge/conflicts.json`, and writes the merged tree to `/merge/out`, which
  starts as the head. When the run succeeds, `/merge/out` is the merge.

Either way the merge version's parents are the head and the merged siblings,
and it becomes the head.

## Versions, retention and pinning

`GET /api/v1/drives/:name/versions` lists versions newest first (`?open=1`:
only the open siblings). Each drive keeps the newest versions your plan
retains; older ones are pruned when a new one is written, except the head,
open siblings (and what they were written against) and versions you pin with
`PATCH /api/v1/drives/:name/versions/:id` `{ "pinned": true }`. Bytes no
kept version holds any more are deleted an hour later.

## Quotas

| Plan | Drives | Stored | Files per version | Versions kept | Mounts per run | Mounted per run | Mounted files per run | Written per run |
|---|---|---|---|---|---|---|---|---|
| Free | 2 | 64 MiB | 512 | 5 | 1 | 4 MiB | 512 | 4 MiB |
| Pro | 25 | 5 GiB | 4,096 | 50 | 4 | 64 MiB | 4,096 | 12 MiB |
| Scale | 250 | 50 GiB | 16,384 | 500 | 8 | 128 MiB | 16,384 | 12 MiB |
| Platform | unlimited | 500 GiB | 16,384 | 1,000 | 8 | 128 MiB | 16,384 | 12 MiB |

Stored bytes count each stored file once however many versions hold it. A
run's mounted bytes add to its filesystem quota rather than using it up. A
read-write run that changes more than its plan's written bytes fails as a
resource limit and stores nothing. A refusal is a 429 naming the limit
(`drive-count-limit`, `drive-storage-limit`, `drive-files-limit`,
`mount-count-limit`, `mount-bytes-limit`, `mount-files-limit`).

## MCP and the SDK

The MCP server has `list_drives`, `create_drive`, `delete_drive`,
`read_drive`, `write_drive`, `list_drive_versions` and `merge_drive`, and
`mounts` on `run_disposable`, `run_code` and `create_sandbox`. The SDK has
`client.drives` (`create`, `list`, `get`, `delete`, `versions`, `files`,
`read`, `write`, `merge`, `pin`) and `mounts` on runs and sandboxes; the
eve provider and the AI SDK session take mounts in the provider shape,
`{ "/data": { "drive": "prices", "mode": "ro" } }`.
