What's new in 0.7.0¶
Published on 2026-10-05: Python SDK rayito 0.7.0, TypeScript SDK
rayito 0.7.0 and rayd 0.7.0.
0.7.0 brings EFS volumes (a live shared file system between sandboxes,
inside your VPC), custom domain as an experimental feature, an
environment for testing locally without an AWS account and security
hardening that runs through rayd, the SDKs, the infrastructure templates
and the release. As always, everything that has a cost is off by default:
without the new options, a sandbox makes no new call to AWS.
At a glance¶
| What's new | Python | TypeScript | Needs | Idle cost | Guide |
|---|---|---|---|---|---|
| EFS volumes (experimental) | volumes={"/mnt/x": vol}, VolumeStore, EfsVolumes |
volumes: { "/mnt/x": vol }, VolumeStore, EfsVolumes |
optional rayito-base-caps-efs image with rayd 0.7.0, a VPC of your own and the efs-volumes stack |
$0 empty; EFS per GB | EFS volumes |
| Custom domain (experimental) | CustomDomain(...) + rayito[custom-domain] extra |
new CustomDomain({...}) + CloudFront peers |
a domain, an ACM certificate and the custom-domain stack |
$0 at rest | Custom domain |
| Test locally | — | — | Docker and a clone of the repository | $0, no AWS account | Test locally |
| Security hardening | no code to change | no code to change | rayd 0.7.0 for what runs in the sandbox |
— | Security |
A few behavior changes ask you to review your configuration before upgrading: they are in How to upgrade from 0.6.x.
EFS volumes¶
An NFS file system in your account that rayd mounts inside the sandbox:
the analog of E2B's Volume. Unlike persist= (an S3 copy that is restored on
create and saved on pause), an EFS volume is shared live among several
sandboxes. Each volume is an EFS access point with its own directory and POSIX
user 1000:1000.
They are experimental for two reasons: they require an optional image and they live inside your VPC. The mount itself is accepted on real AWS in Python and TypeScript: write, read-only, pause and resume (also after the tunnel's credentials expire) and full deletion (Measured on real AWS).
1. Check the VPC and deploy the stack¶
EfsVolumes.check() reviews a VPC that already exists without creating
anything (only ec2:Describe*): subnets in different AZs, free IPs, DNS and
how it would reach the internet. deploy() checks it again and refuses if any
finding is FAIL; if it passes, it creates only new resources inside it (an
encrypted file system, a mount target per subnet, two security groups, a
dedicated egress connector and the execution role's policy) and never touches
the VPC, its subnets, route tables or existing groups.
from rayito import EfsVolumes
efs = EfsVolumes(region="us-east-1") # does not call AWS
report = efs.check(
vpc_id="vpc-0123456789abcdef0",
subnet_ids=["subnet-0123456789abcdef0"],
)
for finding in report.findings:
print(finding.level, finding.code, finding.message)
status = efs.deploy( # refuses if any finding is FAIL
vpc_id="vpc-0123456789abcdef0",
subnet_ids=["subnet-0123456789abcdef0"],
)
print(status.outputs["ConnectorArn"], status.outputs["CallerPolicyArn"])
vol = efs.volume_store().create("agent-data-7")
import { EfsVolumes } from "rayito"; // + npm i @aws-sdk/client-ec2 @aws-sdk/client-efs
const efs = new EfsVolumes({ region: "us-east-1" }); // does not call AWS
const report = await efs.check({
vpcId: "vpc-0123456789abcdef0",
subnetIds: ["subnet-0123456789abcdef0"],
});
for (const finding of report.findings) {
console.log(finding.level, finding.code, finding.message);
}
const status = await efs.deploy({
vpcId: "vpc-0123456789abcdef0",
subnetIds: ["subnet-0123456789abcdef0"],
});
console.log(status.outputs.ConnectorArn, status.outputs.CallerPolicyArn);
const vol = await (await efs.volumeStore()).create("agent-data-7");
Attach the CallerPolicyArn output to the execution role you will launch the
sandboxes with and keep ConnectorArn.
2. Publish the image with amazon-efs-utils¶
The default images do not change. volumes= only mounts on
rayito-base-caps-efs, a caps variant that adds EFS's mount helper (≈ 198
MB more image; the memory snapshot does not change) and that you publish
yourself with --with-efs:
make image-publish-caps-efs BUCKET=amzn-s3-demo-bucket
# or, step by step:
rayito image zip image image/rayito-image-efs.zip --sidecar kernel-sidecar --with-efs
rayito image publish --artifact image/rayito-image-efs.zip --with-efs \
--os-capabilities ALL --bucket amzn-s3-demo-bucket --base-image-version 1
rayito image publish --with-efs checks, before calling AWS, that the zip
carries the variant marker and that you ask for --os-capabilities ALL.
rayito-base-caps-efs also counts as a caps variant for mounts= and
telemetry=.
3. Mount the volumes¶
from rayito import Sandbox, VolumeStore
store = VolumeStore(file_system_id="fs-0123abcd", region="us-east-1")
data = store.create("agent-data-7")
with Sandbox.create(
"rayito-base-caps-efs",
execution_role_arn="arn:aws:iam::123456789012:role/rayito-execution",
egress=["arn:aws:lambda:us-east-1:123456789012:network-connector:rayito-efs"],
volumes={"/mnt/data": data},
) as sbx:
sbx.commands.run("echo hello > /mnt/data/greeting.txt")
print(sbx.volumes["/mnt/data"].state) # "mounted"
import { Sandbox, VolumeStore } from "rayito";
const store = new VolumeStore({ fileSystemId: "fs-0123abcd", region: "us-east-1" });
const data = await store.create("agent-data-7");
await using sbx = await Sandbox.create({
template: "rayito-base-caps-efs",
executionRoleArn: "arn:aws:iam::123456789012:role/rayito-execution",
egress: ["arn:aws:lambda:us-east-1:123456789012:network-connector:rayito-efs"],
volumes: { "/mnt/data": data },
});
await sbx.commands.run("echo hello > /mnt/data/greeting.txt");
console.log((await sbx.volumes()).get("/mnt/data")?.state); // "mounted"
create() validates the request before launching anything (1 to 4 volumes,
paths under /mnt/ or /home/user/, execution_role_arn= and the caps
variant), mounts the volumes in the same ConfigureSandbox as the other
options and only returns with all of them mounted. If one fails, it
terminates the sandbox (unless keep_on_failure) and raises
VolumeMountException (TypeScript: VolumeMountError) with a closed code
(network, iam_denied, not_found, tls, helper_missing, timeout,
invalid_path). reincarnate() mounts them again in the successor. In the
E2B shim, Volume does real CRUD over E2B(volume_store=...) and
volume_mounts= mounts with volume_connector_arn=
(E2B shim).
A single egress connector¶
A MicroVM supports a single egress connector, and the volume needs your
VPC's to reach the mount target. That is why volumes= without egress=,
with INTERNET_EGRESS or with two connectors is InvalidArgumentException
(TypeScript: InvalidArgumentError) before launching anything. A sandbox with
a volume only has internet through your VPC (a NAT or a transit gateway
and a connector that allows it); the efs-volumes stack's only lets NFS out.
check()'s internet-egress finding reminds you of it
(Internet and volumes).
Read-only: through IAM, not through the ro option¶
EfsVolume(read_only=True) mounts with ro, but that alone does not
prevent writing from the sandbox. What makes a volume read-only is the
execution role's policy: deploy the stack with read_only_access_point_arns
(TypeScript: readOnlyAccessPointArns; CLI:
--param ReadOnlyAccessPointArns=...), which denies ClientWrite to those
access points, or with allow_write=False if none should allow writes
(Truly read-only).
Pausing with pending writes and the mount target down loses data
If, when pausing, there are writes that have not yet reached EFS and the
mount target does not respond, rayd cannot flush them in time, AWS
terminates the MicroVM and what was not flushed is lost (pause()
raises SandboxNotFoundException). If a piece of data matters, close it
with fsync or sync before pause()
(What rayd does with each volume).
Delete everything¶
rayito stack destroy efs-volumes deletes the connector, the mount targets,
the security groups and the IAM, but always keeps the file system and its
data. To really delete them:
Full guides: EFS volumes and EFS volumes in your VPC.
Custom domain (experimental)¶
Experimental: not verified end to end on real AWS
CustomDomain is published so you can try it, and its API may change in a
minor. Verified on real AWS: the routing CloudFront Function compiles
and routes in the real runtime (correct token, no token, wrong token,
expired route), and the deployment creates the KeyValueStore and the
Function, with a rollback and a deletion that leave nothing behind.
Not verified: creating the CloudFront distribution and serving traffic
through it (HTTP/1.1, WebSocket, route propagation, JWE renewal), because
the project's test account does not allow creating distributions. Nobody
has yet opened a custom-domain URL deployed with this template.
Without this feature, a sandbox port only answers to whoever sends the AWS
proxy headers that get_host() returns: a browser, a webhook or an
<iframe> cannot. With a custom domain, each registered port has a normal
HTTPS URL under your domain,
https://<port>-<sandbox_id>.sbx.example.com, served by a CloudFront
distribution in your account whose Function checks the traffic token and
sets the proxy headers itself.
import secrets
from rayito import CustomDomain, Sandbox
domain = CustomDomain(public_domain="sbx.example.com") # does not call AWS
domain.deploy(certificate_arn="arn:aws:acm:us-east-1:<account>:certificate/<id>")
with Sandbox.create("rayito-base", allowed_ports=[8000]) as sbx:
jwe = sbx.get_host(8000).headers["x-aws-proxy-auth"]
route = domain.register(
sbx.sandbox_id, 8000, endpoint=sbx.endpoint, jwe=jwe,
traffic_token=secrets.token_urlsafe(32), ttl_seconds=2400,
)
print(f"https://{route.host}")
domain.unregister(sbx.sandbox_id, 8000)
import { randomBytes } from "node:crypto";
import { CustomDomain, Sandbox } from "rayito";
const domain = new CustomDomain({ publicDomain: "sbx.example.com" }); // does not call AWS
await domain.deploy({ certificateArn: "arn:aws:acm:us-east-1:<account>:certificate/<id>" });
await using sbx = await Sandbox.create({ template: "rayito-base", allowedPorts: [8000] });
const jwe = (await sbx.getHost(8000)).headers["x-aws-proxy-auth"] ?? "";
const route = await domain.register(sbx.sandboxId, 8000, {
endpoint: sbx.endpoint,
jwe,
trafficToken: randomBytes(32).toString("base64url"),
ttlSeconds: 2400,
});
console.log(`https://${route.host}`);
await domain.unregister(sbx.sandboxId, 8000);
What is worth knowing:
register()requires a traffic token: a route is only public with an explicitpublic=True(TypeScript:public: true), never by default.deploy(alternate_domain_names=[...])(--alternate-domain-name) replaces the*.sbx.example.comwildcard with exact hostnames.- In Python you need the
pip install "rayito[custom-domain]"extra (the KeyValueStore data plane signs with SigV4A); in TypeScript, the peers@aws-sdk/client-cloudformation,@aws-sdk/client-cloudfront-keyvaluestoreand@aws-sdk/signature-v4a. Without instantiatingCustomDomainnone is loaded. Sandbox.create(domain=)is still not wired up: it raisesUnimplementedErrorwithout calling AWS. Today you register the route yourself androute.hostis the final hostname.
If you try it in an account that allows creating distributions, tell us how it
went in a
GitHub issue
with [custom-domain] in the title, always using example.com and
123456789012 instead of your real data. Guide:
Custom domain.
Test locally with Docker and Floci¶
For those who contribute or want to review a change before spending on AWS: a
Docker Compose environment in the repository runs the Python and TypeScript
SDKs against a real rayd (the product image, with rayd as PID 1) and
against Floci, an AWS emulator. No
account, no credentials and no cost.
make local-up # builds and starts floci, runner and guest
make local-e2e # runs the Python and TypeScript `local` tests
make local-down # stops everything and deletes the volumes
It covers commands, files, PTY, run_code, the pause and resume state
machine, the optional stacks, secrets, the metadata index, events and
webhooks, among others. What depends on the real platform (the AWS proxy,
IMDS, egress, mounts, the memory snapshot, cost) still needs AWS. No container
is privileged, nothing publishes ports on your machine and the emulator has no
internet access. The local-e2e workflow runs it in CI without secrets.
Guide: Test locally.
Security hardening¶
0.7.0 incorporates a full security review. Most changes ask nothing of you
except upgrading; those that change a behavior are in
How to upgrade. The threat model and the detail
of each control are in Security and in
SECURITY.md.
rayd (needs an image with rayd 0.7.0)
- The lifecycle hooks check who opened the connection: the sandbox's code can no longer terminate the sandbox, repeat the validation or impersonate the platform's startup that way, and each attempt is counted as an anomaly.
- File operations work on descriptors without following symbolic links, and
rayd's internal processes start with a minimal environment. - The agent's ports have a connection cap, the identity of the user's
processes has a closed range and
raydchecks kernel processes before signaling them. persist=binds the sandbox to its persistence prefix: with a per-tenant prefix, a sandbox cannot reach another's persistedHOMEeven if they share an execution role.- The secret gateway validates routes against an allowlist (also in the SDK, before any call) and no longer returns the injected credential in the response headers.
Two of these controls (the startup origin and the connection cap) remain pending acceptance on real AWS.
rayito sandbox proxy: it only forwards requests whose Host and
Origin are the expected ones (loopback, <id>.localhost or those you
declare with --allowed-host and --allow-origin), a single message per
connection and at most --max-connections at a time. A web page open in your
browser can no longer use the sandbox's service with your token
(Local proxy).
Templates and images: Template.build does not follow symbolic links,
applies .dockerignore with Docker's semantics and warns if the context is
about to package sensitive files (.env*, .git/, keys). rayito image zip
and make image-publish reject symbolic links in the image tree
(.dockerignore).
Git credentials: clone/push/pull with username and password always
restore the URL without credentials, run without hooks or credential
helpers and refuse if the git configuration rewrites URLs. Even so, treat the
token as revealed to the sandbox's code: use short-lived, single-repository
tokens (Git).
IAM and the optional stacks' Lambdas
infra/iam.yamlseparatesSandboxLauncherPolicy(launch only) fromImagePublisherPolicy(publish only);CallerPolicyremains the union.RayitoTemplateBuilderonly readsrayito/*from the bucket, andsizes-guardalso denies publishing images.events-webhooks: the forwarder authenticates each line before reading it, discards old, repeated events or events that do not advance the lifecycle, rate-limitspaused/resumedper sandbox and does not lose the batch if a write fails. A broken or slow webhook no longer blocks the others, each delivery also carries the timestampedrayito-signaturesignature and a rotated secret stops being used in 5 minutes. Each Lambda writes only its rows and its logs, and the caller has one policy per task (Which policy each call needs).- Webhook signing secrets are kept out of
secrets=, and each optional stack's zip is uploaded torayito/stacks/<component>/and compared before being reused.
SDK and CLI: AWS errors are sanitized on more paths, a command's or PTY's
output is kept with a cap of 64 MiB per descriptor
(Stored output), the CLI neutralizes
escape sequences in logs and messages, a custom access token must have at
least 16 bytes and the proxy JWE no longer appears in repr or in
console.log.
Supply chain: signing rayd and the image requires the maintainer's
approval, assets are built without credentials and cannot be replaced from the
release, the release does not restore caches and pins its tools by version and
hash, and the verification recipe binds the signature to the exact tag you
install (Verify a release).
How to upgrade from 0.6.x¶
-
Upgrade the SDK.
-
Republish your image on
rayd0.7.0 (Set up AWS). The compatibility series moves to0.7:rayito doctorrequires theraydfrom therayd-v0.7.0tag for SDK 0.7 (Compatibility), becausevolumes=and the agent hardening live inrayd. Against an earlier image,volumes=fails closed withUnimplementedErrorand terminates the sandbox it just launched. - Redeploy the stacks you use.
rayito events deployuploads the new code of theevents-webhooksLambdas; also redeployinfra/iam.yamlto get the separated policies (IAM).EventsOperatorPolicystill exists in 0.7 but is deprecated: attachEventsLauncherPolicy,EventsReaderPolicyorEventsWebhookAdminPolicyto each identity. - Review these behavior changes:
| What | Before | Since 0.7.0 |
|---|---|---|
rayito sandbox proxy --bind 0.0.0.0 |
--allow-remote was enough |
requires --allowed-host; a different Host (e.g. behind a reverse proxy) gets 421 without --allowed-host, a foreign Origin 403, and a request with ambiguous delimiting 400 |
.dockerignore of Template.build |
* crossed / |
* and ? do not cross /: *.pyc only excludes those at the root; use **/*.pyc |
Custom access token (access_token=, RAYITO_ACCESS_TOKEN) |
any length | at least 16 decoded bytes, or InvalidArgumentException / InvalidArgumentError |
Explicit checkpoint or restore toward another base from a sandbox with persist= |
allowed | permission_denied |
Output of commands.run and PTY |
no cap | 64 MiB per descriptor (the end is kept and truncated says so); adjustable with max_output_bytes / maxOutputBytes |
allow route in SecretGateway that rayd would never let through |
accepted | InvalidArgumentException / InvalidArgumentError before calling AWS |
secrets= with a rayito/webhooks/* secret |
accepted | InvalidArgumentException / InvalidArgumentError |
register_webhook with an unreachable URL (invalid port or host) |
accepted | InvalidArgumentException / InvalidArgumentError |
git with credentials and url.*.insteadOf in the configuration |
carried on | GitAuthException / GitAuthError |
Without new options, the rest of your code stays the same: there are no incompatible changes in the public API from 0.6.x outside this table.
What 0.7.0 does not bring¶
Sandbox.create(domain=)andget_host()/expose()returning the custom domain's URL: they remain in development.- The
Volumecontent operations of the shim (read_file,write_file... without a sandbox): Rayito has no data plane outside a MicroVM, so they keep raisingUnimplementedError.
Full details, change by change: Changelog.