123456789101112131415161718import { Sandbox } from "@opencomputer/sdk"; const sandbox = await Sandbox.create(); await sandbox.mounts.add({ path: "/mnt/data", remote: "s3:my-bucket/datasets", backend: "s3", creds: { access_key_id: process.env.AWS_ACCESS_KEY_ID!, secret_access_key: process.env.AWS_SECRET_ACCESS_KEY!, region: "us-east-1", }, }); // Now anything in the sandbox can read s3://my-bucket/datasets as /mnt/data: await sandbox.exec.run("ls /mnt/data"); await sandbox.exec.run("head /mnt/data/train.csv");
rclone mount under the
hood — a single binary that speaks ~40 backends (S3, GCS, Azure Blob, SFTP,
WebDAV, Dropbox, and more). When you call mounts.add():backend + creds.0600 — the
sandbox user can't read it).rclone mount is spawned as a background daemon, exposing the remote at
the path you specified.backend selects how creds are templated. The keys you pass map directly to
rclone config fields for that backend type.| Backend | Common keys |
s3 | access_key_id, secret_access_key, region, provider (default AWS), endpoint |
gcs | service_account_credentials (JSON string) or service_account_file |
azureblob | account, key or sas_url |
sftp | host, user, pass or key_file, port |
webdav | url, vendor, user, pass |
dropbox | token |
rcloneConfig
directly with a raw rclone config string — see Custom Config
below.12345678910await sandbox.mounts.add({ path: "/mnt/data", remote: "s3:my-bucket", backend: "s3", creds: { access_key_id: process.env.AWS_ACCESS_KEY_ID!, secret_access_key: process.env.AWS_SECRET_ACCESS_KEY!, region: "us-east-1", }, });
provider and point at the endpoint:123456789101112await sandbox.mounts.add({ path: "/mnt/r2", remote: "r2:my-bucket", backend: "s3", creds: { provider: "Cloudflare", access_key_id: "...", secret_access_key: "...", endpoint: "https://<accountid>.r2.cloudflarestorage.com", region: "auto", }, });
12345678910import fs from "node:fs"; await sandbox.mounts.add({ path: "/mnt/gcs", remote: "gcs:my-bucket", backend: "gcs", creds: { service_account_credentials: fs.readFileSync("./sa-key.json", "utf8"), }, });
12345678910await sandbox.mounts.add({ path: "/mnt/remote", remote: "ssh:/home/data", backend: "sftp", creds: { host: "data.example.com", user: "agent", pass: process.env.SSH_PASSWORD!, // or key_file }, });
--vfs-cache-mode
or --dir-cache-time — pass an rclone config string directly. The section
name in the config must match the part of remote before the colon.12345678910await sandbox.mounts.add({ path: "/mnt/box", remote: "box:Reports", rcloneConfig: ` [box] type = box token = {"access_token":"...","refresh_token":"...","expiry":"..."} `.trim(), mountOptions: ["--dir-cache-time", "1m"], });
1234567await sandbox.mounts.add({ path: "/mnt/writable", remote: "s3:scratch", backend: "s3", creds: {...}, readOnly: false, // opts into RW; uses --vfs-cache-mode writes });
1234const mounts = await sandbox.mounts.list(); // [{ path: "/mnt/data", remote: "s3:my-bucket", backend: "s3", readOnly: true }] await sandbox.mounts.remove("/mnt/data");
123456await sandbox.mounts.add({ path: "/mnt/data", remote: "s3:my-bucket", backend: "s3", creds }); await sandbox.hibernate(); await sandbox.wake(); await sandbox.exec.run("ls /mnt/data"); // still works, no re-mount needed
remove() explicitly:1await sandbox.mounts.remove("/mnt/data");
fusermount3 -u in the VM and drops the registry
entry. No magic teardown on hibernate.mounts.remove(path) then mounts.add(...) again with fresh
credentials.12345678910oc mounts add sb-abc123 \ --path /mnt/data \ --remote s3:my-bucket \ --backend s3 \ --cred access_key_id=AKIA... \ --cred secret_access_key=... \ --cred region=us-east-1 oc mounts list sb-abc123 oc mounts rm sb-abc123 /mnt/data
--config-file at a local file:1oc mounts add sb-abc123 --path /mnt/box --remote box:Reports --config-file ./rclone.conf
sandbox image is missing rclone and/or fusermount3 — the sandbox is
running on an older base image. Recreate the sandbox from the latest default
template, or build an image off the current Dockerfile.default.ls returns empty — usually a creds /
permission issue on the remote side. SSH in (oc shell) and run
rclone ls <remote>: --config /run/oc-agent/mounts/<id>.conf to see the real
error. Or ls -la the mount point — FUSE errors surface as filesystem errors.fuse: bad mount point ... permission denied — make sure the target path
isn't already a non-empty directory you don't own. The mount call creates the
path with sandbox:sandbox ownership; pre-existing roots can conflict.mounts.add() picks a driver — the daemon that actually
establishes the FUSE mount. Everything above uses the default rclone driver.
If you already have a FUSE-ready filesystem (your own VFS, gcsfuse, s3fs, …) and
don't want rclone as a middle layer, use the command driver: you hand us
the daemon's argv and we run it as the sandbox user, inject your env/secrets,
wait for the mount to come live, and tear it down on remove — the same
lifecycle as an rclone mount.123456await sandbox.mounts.add({ path: "/mnt/data", driver: "command", command: ["my-vfs-fuse", "--bucket", "gs://my-bucket", "{mountpoint}"], secrets: { MY_VFS_TOKEN: process.env.MY_VFS_TOKEN! }, });
mounts.add_command(...).{mountpoint} token{mountpoint} (or {path}) in your argv is replaced with path before the
command runs, so you don't repeat the path twice. The call above runs:1my-vfs-fuse --bucket gs://my-bucket /mnt/data
env — plain environment variables for the daemon. Recorded and returned
by list().secrets — credentials. Injected into the daemon's process
environment (never the command line, so they don't leak via ps), never
written to disk on the worker, and never returned by list().readOnly is advisory for this driver — your command must honor it (we also
export OC_MOUNT_READONLY=1). Unlike rclone, the platform can't enforce
read-only on an arbitrary daemon.remove
cleans up./dev/fuse access, creating the mountpoint, and
-o allow_other support (user_allow_other is set in /etc/fuse.conf). If the
mount doesn't come up within the timeout, the call fails with the daemon's own
log tail so you can see why (a bad flag, missing binary, auth error, etc.).1234567891011121314await sandbox.files.write("/run/gcp-sa.json", process.env.GCP_SA_JSON!); await sandbox.mounts.add({ path: "/mnt/bucket", driver: "command", command: [ "gcsfuse", "--foreground", "--implicit-dirs", "-o", "allow_other", "--key-file", "/run/gcp-sa.json", "my-bucket", "{mountpoint}", ], }); await sandbox.exec.run("ls /mnt/bucket"); // GCS objects, read like local files
--anonymous-access:12command: ["gcsfuse", "--foreground", "--anonymous-access", "-o", "allow_other", "gcp-public-data-landsat", "{mountpoint}"],
12345678await sandbox.exec.run("apt-get update && apt-get install -y bindfs"); await sandbox.mounts.add({ path: "/mnt/mirror", driver: "command", command: ["bindfs", "-f", "-o", "allow_other", "/home/sandbox/data", "{mountpoint}"], readOnly: false, }); // reads/writes under /mnt/mirror now pass through to /home/sandbox/data
--command once per argv element; --env/--secret take key=value:12345oc mounts add sb-abc123 \ --path /mnt/mirror \ --command bindfs --command -f --command -o --command allow_other \ --command /home/sandbox/data --command '{mountpoint}' \ --secret MY_VFS_TOKEN=...
oc shell and run the same command.