S3 mounts¶
Mounts one or more S3 buckets (or a prefix of one) as a normal folder
inside the sandbox, with Mountpoint for Amazon S3
(mount-s3). Only on rayito-base-caps (or a size-derived variant):
mount(2) needs CAP_SYS_ADMIN, which only that variant gives the agent,
and the daemon needs the execution role's credentials through IMDS. On any
other image, create() terminates the VM and raises
UnimplementedError.
Cost and activation
- Default: off. Without
mounts=(TypeScript:mounts)rayddoes not open/dev/fuseor launch anything. - Turns on:
mounts={"/mnt/data": S3Mount(bucket="...", prefix="...")}inSandbox.create(). It needs three things: a publishedrayito-base-capsimage with the bucket in its allowlist (RAYITO_ALLOWED_MOUNT_BUCKETS, see below),execution_role_arn=and thes3-mountsstack's IAM policy on that role. - Resources and AWS calls: nothing new by itself.
mount-s3makes the normal S3 calls (GetObject/ListObjectsV2, andPutObject/DeleteObjectwith write access) against the bucket you mount, under the execution role's credentials — the same ones you would already pay for by calling S3 yourself. - Approximate cost (us-east-1, checked 2026-10-01, S3 pricing): $0 of its own from Rayito; you pay the normal S3 requests and storage of the bucket you mount.
- IAM: the managed policy
RayitoS3MountAccess(infra/s3-mounts.yaml,OptionalStackcomponents3-mounts) on the sandbox's execution role, scoped to one bucket per stack. - How to turn it off: do not pass
mounts=;rayito stack destroy s3-mountsremoves the policy (it deletes no object or the bucket).
Measured on real AWS
Sandbox.create(mounts=)/create({ mounts }) and sbx.mounts passed
acceptance on real AWS (S3M-1..S3M-4, AWS_API_NOTES.md
Q100–Q104): read and write, pause()/resume() with the mount
live, allow_internet_access=False, and the VM terminated on a mount
failure. mount-s3 1.24.0 adds 72.7 MB to the image.
When to use it¶
- Your code already knows how to work with normal paths (
pandas.read_csv,ls,grep, a library that only accepts a path) and the data lives in S3: without a mount, you would have to download it all first. - You want an agent's output (files it writes) to show up directly in S3, without an extra upload step.
- When not to: you need full POSIX semantics (locks, writable
mmap, very many very small files with a lot offsync) — S3 is not a filesystem, andmount-s3has the same limit as any FUSE over an object store. For that, copy the file into the sandbox'sHOMEwithfiles.write/sandbox.gitinstead of mounting it.
Mount paths¶
The key of mounts=/mounts is the absolute path where the bucket appears
inside the guest: under /mnt/ or /home/user/, not overlapping another
mount (the same rule volumes= uses for
EFS volumes), and at most 4 mounts in total per
sandbox, adding up mounts= and volumes=.
Before you start: the image allowlist (mandatory)¶
rayd only mounts the buckets the image itself declares in
RAYITO_ALLOWED_MOUNT_BUCKETS (comma-separated). It is image
configuration, not a per-sandbox option: that way the code calling
create() cannot mount a bucket the person who publishes the image did not
authorize. Empty or absent denies every bucket: create() terminates
the VM and raises MountException(code="not_allowed") (MountError with
code: "not_allowed" in TypeScript), whose message says the bucket is not
in the image's allowlist and how to publish it with the bucket (it never
names the bucket).
rayito image publish --artifact rayito-image.zip --base-image-version 1 \
--image-name rayito-base-caps --os-capabilities ALL \
--env RAYITO_ALLOWED_MOUNT_BUCKETS=my-bucket,other-bucket
From the repository, make image-publish-caps MOUNT_BUCKETS=my-bucket,other-bucket
(or image-publish-caps-efs) adds that --env for you.
If you build the image with your own Dockerfile on top of
rayito-base-caps, ENV RAYITO_ALLOWED_MOUNT_BUCKETS=... does the same.
Changing the allowlist means publishing a new version of the image.
Deploy the IAM policy¶
rayito stack deploy s3-mounts --param BucketName=my-bucket \
--param Prefixes='team7/*,runs/*' --param ReadOnly=false
The RayitoS3MountAccess policy covers a single bucket (one stack per
bucket) and, within it, only the Prefixes prefixes (up to 4, ending in
*; * by default is the whole bucket), also for reading, writing and
deleting objects: a sandbox that should only write runs/42/ cannot touch
objects under another prefix. rayito stack status s3-mounts shows the ARN
of the policy already created; add it to the sandbox's execution role.
rayito stack destroy s3-mounts removes it (it never deletes the bucket or
its objects).
Example¶
create() does not return until every mount is mounted: if one fails (or
still does not answer after 15 s), it terminates the VM and raises
MountException with the reason in code (iam_denied, not_found,
not_allowed, invalid_path, helper_missing, network or timeout;
unknown if the agent sends a class your SDK version does not know yet).
As soon as create() returns, the folder can be read.
from rayito import S3Mount, Sandbox
sbx = Sandbox.create(
"rayito-base-caps",
execution_role_arn="arn:aws:iam::<account>:role/my-execution-role",
mounts={
"/mnt/data": S3Mount(bucket="my-bucket", prefix="team7/"), # (1)!
"/mnt/out": S3Mount(
bucket="my-bucket", prefix="runs/42/", read_only=False, allow_overwrite=True
),
},
)
sbx.commands.run("python -c \"import pandas; pandas.read_csv('/mnt/data/data.csv')\"")
print(sbx.mounts) # {"/mnt/data": MountStatus(state="mounted"), ...}
sbx.kill()
read_only=Trueby default: read-only unless you explicitly ask otherwise.
import { Sandbox, S3Mount } from "rayito";
const sbx = await Sandbox.create({
template: "rayito-base-caps",
executionRoleArn: "arn:aws:iam::<account>:role/my-execution-role",
mounts: {
"/mnt/data": new S3Mount({ bucket: "my-bucket", prefix: "team7/" }),
"/mnt/out": new S3Mount({
bucket: "my-bucket",
prefix: "runs/42/",
readOnly: false,
allowOverwrite: true,
}),
},
});
await sbx.commands.run("python3 -c \"import pandas; pandas.read_csv('/mnt/data/data.csv')\"");
console.log(await sbx.mounts()); // Map { "/mnt/data" => { state: "mounted" }, ... }
await sbx.kill();
mounts also accepts a Map in TypeScript.
sbx.mounts (a property on Sandbox; await sbx.mounts() on
AsyncSandbox and in TypeScript) reads the live state on every call: if a
mount's daemon dies later, rayd relaunches it on its own and, in the
meantime, that mount shows up as "pending" or "failed".
Differences from E2B¶
E2B has no equivalent to mounts= (row 111 of
E2B parity): the shim adds no new method for this.
Security¶
- The
mount-s3daemon runs as the dedicated userrayito-mount(uid 990), never as uid 1000 or as root after starting; its credentials arrive through its own IMDS access, never through argv or through the environment of the processraydlaunches (AWS_REGION/PATHare the only variables). - The bucket and prefix are not secrets (like the sandbox's
metadata): a uid 1000 process can see the bucket name and the prefix on the daemon's command line (/proc/<pid>/cmdline), never a credential; its environment (/proc/<pid>/environ) is not even readable, and it cannot send it signals either. - The bucket must be in the image's
RAYITO_ALLOWED_MOUNT_BUCKETS(image config, not a per-sandbox option); empty or absent denies every bucket. - The availability probe
raydlaunches as uid 1000 after each mount (/usr/bin/stat <path>) andmount-s3itself start with an environment built from scratch (onlyPATH, plusAWS_REGIONformount-s3), inheriting no descriptor fromraydand none of its ignored signals:rayd's environment (the image ARN,RAYITO_*variables or the image's--env) never reaches a process another uid 1000 process can read in/proc/<pid>/environ. raydmounts as root, but it never follows a symbolic link in the mount path (neither when creating it nor when relaunching a mount): if the sandbox code replaces/home/user/<folder>with a link to a system directory, the mount fails withinvalid_pathinstead of mounting the bucket on top of that directory.- The IAM policy is scoped to the prefixes you declare, also for writing
and deleting: containment does not depend only on
mount-s3 --prefixinside a guest that runs untrusted code.
Full detail: Security, threat T20.