Skip to content
Shotcup

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

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

{
  "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

PlanDrivesStoredFiles per versionVersions keptMounts per runMounted per runMounted files per runWritten per run
Free264 MiB512514 MiB5124 MiB
Pro255 GiB4,09650464 MiB4,09612 MiB
Scale25050 GiB16,3845008128 MiB16,38412 MiB
Platformunlimited500 GiB16,3841,0008128 MiB16,38412 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" } }.

This page as Markdown:/docs/drives.md