Saltar a contenido

Novedades de 0.7.0

Publicada el 2026-10-05: SDK Python rayito 0.7.0, SDK TypeScript rayito 0.7.0 y rayd 0.7.0.

0.7.0 trae los volúmenes EFS (un sistema de ficheros compartido en vivo entre sandboxes, dentro de tu VPC), el dominio propio como función experimental, un entorno para probar en local sin cuenta de AWS y un endurecimiento de seguridad que recorre rayd, los SDK, las plantillas de infraestructura y la release. Como siempre, todo lo que tiene coste está apagado por defecto: sin las opciones nuevas, un sandbox no hace ninguna llamada nueva a AWS.

De un vistazo

Novedad Python TypeScript Necesita Coste en reposo Guía
Volúmenes EFS (experimental) volumes={"/mnt/x": vol}, VolumeStore, EfsVolumes volumes: { "/mnt/x": vol }, VolumeStore, EfsVolumes imagen opcional rayito-base-caps-efs con rayd 0.7.0, una VPC tuya y la pila efs-volumes $0 vacío; EFS por GB Volúmenes EFS
Dominio propio (experimental) CustomDomain(...) + extra rayito[custom-domain] new CustomDomain({...}) + peers de CloudFront un dominio, un certificado ACM y la pila custom-domain $0 en reposo Dominio propio
Probar en local — — Docker y un clon del repositorio $0, sin cuenta de AWS Probar en local
Endurecimiento de seguridad sin código que cambiar sin código que cambiar rayd 0.7.0 para lo que corre en el sandbox — Seguridad

Unos pocos cambios de comportamiento piden revisar tu configuración antes de actualizar: están en Cómo actualizar desde 0.6.x.

Volúmenes EFS

Un sistema de ficheros NFS en tu cuenta que rayd monta dentro del sandbox: el análogo de Volume de E2B. A diferencia de persist= (una copia en S3 que se restaura al crear y se guarda al pausar), un volumen EFS se comparte en vivo entre varios sandboxes. Cada volumen es un access point de EFS con su propio directorio y el usuario POSIX 1000:1000.

Son experimentales por dos motivos: exigen una imagen opcional y viven dentro de tu VPC. El montaje en sí está aceptado en AWS real en Python y TypeScript: escritura, sólo lectura, pausa y reanudación (también tras caducar las credenciales del túnel) y el borrado completo (Medido en AWS real).

1. Comprueba la VPC y despliega la pila

EfsVolumes.check() revisa una VPC que ya existe sin crear nada (sólo ec2:Describe*): subredes en AZs distintas, IPs libres, DNS y cómo saldría a internet. deploy() vuelve a comprobarla y se niega si algún hallazgo es FAIL; si pasa, crea sólo recursos nuevos dentro de ella (un sistema de ficheros cifrado, un mount target por subred, dos grupos de seguridad, un conector de egress dedicado y la política del execution role) y nunca toca la VPC, sus subredes, tablas de rutas ni grupos existentes.

from rayito import EfsVolumes

efs = EfsVolumes(region="us-east-1")  # no llama a 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(  # se niega si algún hallazgo es FAIL
    vpc_id="vpc-0123456789abcdef0",
    subnet_ids=["subnet-0123456789abcdef0"],
)
print(status.outputs["ConnectorArn"], status.outputs["CallerPolicyArn"])
vol = efs.volume_store().create("datos-agente-7")
import { EfsVolumes } from "rayito"; // + npm i @aws-sdk/client-ec2 @aws-sdk/client-efs

const efs = new EfsVolumes({ region: "us-east-1" }); // no llama a 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("datos-agente-7");
rayito doctor --efs-vpc-id vpc-0123456789abcdef0 \
  --efs-subnet-ids subnet-0123456789abcdef0
rayito stack deploy efs-volumes \
  --param VpcId=vpc-0123456789abcdef0 \
  --param SubnetIds=subnet-0123456789abcdef0
rayito stack status efs-volumes

Adjunta la salida CallerPolicyArn al execution role con el que lanzarás los sandboxes y guarda ConnectorArn.

2. Publica la imagen con amazon-efs-utils

Las imágenes por defecto no cambian. volumes= sólo monta en rayito-base-caps-efs, una variante caps que añade el mount helper de EFS (≈ 198 MB más de imagen; el snapshot de memoria no cambia) y que publicas tú con --with-efs:

make image-publish-caps-efs BUCKET=amzn-s3-demo-bucket
# o, paso a paso:
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 comprueba, antes de llamar a AWS, que el zip trae el marcador de la variante y que pides --os-capabilities ALL. rayito-base-caps-efs cuenta además como variante caps para mounts= y telemetry=.

3. Monta los volúmenes

from rayito import Sandbox, VolumeStore

store = VolumeStore(file_system_id="fs-0123abcd", region="us-east-1")
datos = store.create("datos-agente-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/datos": datos},
) as sbx:
    sbx.commands.run("echo hola > /mnt/datos/saludo.txt")
    print(sbx.volumes["/mnt/datos"].state)  # "mounted"
import { Sandbox, VolumeStore } from "rayito";

const store = new VolumeStore({ fileSystemId: "fs-0123abcd", region: "us-east-1" });
const datos = await store.create("datos-agente-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/datos": datos },
});
await sbx.commands.run("echo hola > /mnt/datos/saludo.txt");
console.log((await sbx.volumes()).get("/mnt/datos")?.state); // "mounted"

create() valida la petición antes de lanzar nada (de 1 a 4 volúmenes, rutas bajo /mnt/ o /home/user/, execution_role_arn= y la variante caps), monta los volúmenes en el mismo ConfigureSandbox que el resto de opciones y sólo vuelve con todos mounted. Si uno falla, termina el sandbox (salvo keep_on_failure) y lanza VolumeMountException (TypeScript: VolumeMountError) con un code cerrado (network, iam_denied, not_found, tls, helper_missing, timeout, invalid_path). reincarnate() vuelve a montarlos en el sucesor. En el shim de E2B, Volume hace CRUD real sobre E2B(volume_store=...) y volume_mounts= monta con volume_connector_arn= (Shim E2B).

Un solo conector de egress

Un MicroVM admite un único conector de egress, y el volumen necesita el de tu VPC para llegar al mount target. Por eso volumes= sin egress=, con INTERNET_EGRESS o con dos conectores es InvalidArgumentException (TypeScript: InvalidArgumentError) antes de lanzar nada. Un sandbox con volumen sólo tiene internet a través de tu VPC (una NAT o un transit gateway y un conector que lo permita); el de la pila efs-volumes sólo deja salir NFS. El hallazgo internet-egress de check() te lo recuerda (Internet y volúmenes).

Sólo lectura: por IAM, no por la opción ro

EfsVolume(read_only=True) monta con ro, pero eso solo no impide escribir desde el sandbox. Lo que hace de sólo lectura un volumen es la política del execution role: despliega la pila con read_only_access_point_arns (TypeScript: readOnlyAccessPointArns; CLI: --param ReadOnlyAccessPointArns=...), que le deniega ClientWrite a esos access points, o con allow_write=False si ninguno debe admitir escritura (Sólo lectura de verdad).

Pausar con escrituras pendientes y el mount target caído pierde datos

Si al pausar hay escrituras que aún no llegaron a EFS y el mount target no responde, rayd no puede vaciarlas a tiempo, AWS termina el MicroVM y lo no vaciado se pierde (pause() lanza SandboxNotFoundException). Si un dato importa, ciérralo con fsync o sync antes de pause() (Lo que hace rayd con cada volumen).

Borrarlo todo

rayito stack destroy efs-volumes borra el conector, los mount targets, los grupos de seguridad y el IAM, pero siempre conserva el sistema de ficheros y sus datos. Para borrarlos de verdad:

from rayito import EfsVolumes

EfsVolumes(region="us-east-1").destroy(delete_file_system=True)
import { EfsVolumes } from "rayito";

await new EfsVolumes({ region: "us-east-1" }).destroy({ deleteFileSystem: true });

Guías completas: Volúmenes EFS y Volúmenes EFS en tu VPC.

Dominio propio (experimental)

Experimental: no verificado de punta a punta en AWS real

CustomDomain se publica para que puedas probarlo, y su API puede cambiar en una minor. Verificado en AWS real: la CloudFront Function de enrutado compila y enruta en el runtime real (token correcto, sin token, token erróneo, ruta caducada), y el despliegue crea el KeyValueStore y la Function, con un rollback y un borrado que no dejan nada. Sin verificar: crear la distribución CloudFront y servir tráfico por ella (HTTP/1.1, WebSocket, propagación de rutas, renovación del JWE), porque la cuenta de pruebas del proyecto no permite crear distribuciones. Nadie ha abierto todavía una URL de dominio propio desplegada con esta plantilla.

Sin esta función, un puerto del sandbox sólo responde a quien manda las cabeceras del proxy de AWS que devuelve get_host(): un navegador, un webhook o un <iframe> no pueden. Con dominio propio, cada puerto registrado tiene una URL HTTPS normal bajo tu dominio, https://<puerto>-<sandbox_id>.sbx.example.com, servida por una distribución CloudFront en tu cuenta cuya Function comprueba el token de tráfico y pone ella misma las cabeceras del proxy.

import secrets

from rayito import CustomDomain, Sandbox

domain = CustomDomain(public_domain="sbx.example.com")  # no llama a AWS
domain.deploy(certificate_arn="arn:aws:acm:us-east-1:<cuenta>: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" }); // no llama a AWS
await domain.deploy({ certificateArn: "arn:aws:acm:us-east-1:<cuenta>: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);
rayito domain deploy --public-domain sbx.example.com \
  --certificate-arn arn:aws:acm:us-east-1:<cuenta>:certificate/<id>
rayito domain status
rayito domain destroy

Lo que conviene saber:

  • register() exige un token de tráfico: una ruta sólo es pública con public=True (TypeScript: public: true) explícito, nunca por omisión.
  • deploy(alternate_domain_names=[...]) (--alternate-domain-name) sustituye el comodín *.sbx.example.com por hostnames exactos.
  • En Python necesitas el extra pip install "rayito[custom-domain]" (el plano de datos del KeyValueStore firma con SigV4A); en TypeScript, los peers @aws-sdk/client-cloudformation, @aws-sdk/client-cloudfront-keyvaluestore y @aws-sdk/signature-v4a. Sin instanciar CustomDomain no se carga ninguno.
  • Sandbox.create(domain=) sigue sin cablear: lanza UnimplementedError sin llamar a AWS. Hoy registras la ruta tú mismo y route.host es el hostname final.

Si lo pruebas en una cuenta que permita crear distribuciones, cuéntanos cómo te fue en un issue de GitHub con [custom-domain] en el título, usando siempre example.com y 123456789012 en lugar de tus datos reales. Guía: Dominio propio.

Probar en local con Docker y Floci

Para quien contribuye o quiere revisar un cambio antes de gastar en AWS: un entorno de Docker Compose en el repositorio corre los SDK de Python y TypeScript contra un rayd de verdad (la imagen de producto, con rayd como PID 1) y contra Floci, un emulador de AWS. Sin cuenta, sin credenciales y sin coste.

make local-up      # construye y arranca floci, runner y guest
make local-e2e     # corre los tests `local` de Python y de TypeScript
make local-down    # lo para todo y borra los volúmenes

Cubre comandos, ficheros, PTY, run_code, la máquina de estados de pausa y reanudación, las pilas opcionales, secretos, el índice de metadatos, eventos y webhooks, entre otros. Lo que depende de la plataforma real (el proxy de AWS, IMDS, el egress, los montajes, el snapshot de memoria, el coste) sigue necesitando AWS. Ningún contenedor es privilegiado, nada publica puertos en tu máquina y el emulador no tiene salida a internet. El workflow local-e2e lo corre en CI sin secretos. Guía: Probar en local.

Endurecimiento de seguridad

0.7.0 incorpora una revisión de seguridad completa. La mayoría de los cambios no piden nada de tu parte salvo actualizar; los que cambian un comportamiento están en Cómo actualizar. El modelo de amenazas y el detalle de cada control están en Seguridad y en SECURITY.md.

rayd (necesita una imagen con rayd 0.7.0)

  • Los hooks de ciclo de vida comprueban quién abrió la conexión: el código del sandbox ya no puede terminar el sandbox, repetir la validación ni suplantar el arranque de la plataforma por esa vía, y cada intento queda contado como anomalía.
  • Las operaciones de ficheros trabajan sobre descriptores sin seguir enlaces simbólicos, y los procesos internos de rayd arrancan con un entorno mínimo.
  • Los puertos del agente tienen un tope de conexiones, la identidad de los procesos del usuario tiene un rango cerrado y rayd comprueba los procesos de kernel antes de señalarlos.
  • persist= liga el sandbox a su prefijo de persistencia: con un prefijo por inquilino, un sandbox no alcanza el HOME persistido de otro aunque compartan execution role.
  • La pasarela de secretos valida las rutas con una lista de permitidos (también en el SDK, antes de cualquier llamada) y ya no devuelve la credencial inyectada en las cabeceras de la respuesta.

Dos de estos controles (el origen del arranque y el tope de conexiones) quedan pendientes de aceptación en AWS real.

rayito sandbox proxy: sólo reenvía peticiones cuyo Host y Origin son los esperados (loopback, <id>.localhost o los que declares con --allowed-host y --allow-origin), un solo mensaje por conexión y como mucho --max-connections a la vez. Una web abierta en tu navegador ya no puede usar el servicio del sandbox con tu token (Proxy local).

Templates e imágenes: Template.build no sigue enlaces simbólicos, aplica .dockerignore con la semántica de Docker y avisa si el contexto va a empaquetar ficheros sensibles (.env*, .git/, claves). rayito image zip y make image-publish rechazan enlaces simbólicos en el árbol de la imagen (.dockerignore).

Credenciales de git: clone/push/pull con usuario y contraseña restauran siempre la URL sin credenciales, corren sin hooks ni credential helpers y se niegan si la configuración de git reescribe URLs. Aun así, trata el token como revelado al código del sandbox: usa tokens de vida corta y de un solo repositorio (Git).

IAM y Lambdas de las pilas opcionales

  • infra/iam.yaml separa SandboxLauncherPolicy (sólo lanzar) de ImagePublisherPolicy (sólo publicar); CallerPolicy sigue siendo la unión. RayitoTemplateBuilder sólo lee rayito/* del bucket, y sizes-guard niega también publicar imágenes.
  • events-webhooks: el forwarder autentica cada línea antes de leerla, descarta eventos viejos, repetidos o que no hacen avanzar el ciclo de vida, limita paused/resumed por sandbox y no pierde el lote si falla una escritura. Un webhook roto o lento ya no bloquea a los demás, cada entrega lleva además la firma con marca de tiempo rayito-signature y un secreto rotado deja de usarse en 5 minutos. Cada Lambda escribe sólo sus filas y sus logs, y el llamante tiene una política por tarea (Qué política necesita cada llamada).
  • Los secretos de firma de webhooks quedan fuera de secrets=, y el zip de cada pila opcional se sube a rayito/stacks/<componente>/ y se compara antes de reutilizarlo.

SDK y CLI: los errores de AWS se sanean en más caminos, la salida de un comando o PTY se guarda con un tope de 64 MiB por descriptor (Salida guardada), la CLI neutraliza secuencias de escape de logs y mensajes, un access token propio debe tener al menos 16 bytes y el JWE del proxy ya no aparece en repr ni en console.log.

Cadena de suministro: firmar rayd y la imagen exige la aprobación del mantenedor, los assets se construyen sin credenciales y no se pueden reemplazar desde la release, la release no restaura cachés y fija sus herramientas por versión y hash, y la receta de verificación liga la firma al tag exacto que instalas (Verificar una release).

Cómo actualizar desde 0.6.x

  1. Actualiza el SDK.

    pip install -U "rayito[cli]"    # con dominio propio: "rayito[cli,custom-domain]"
    
    pnpm add rayito@latest
    
  2. Republica tu imagen sobre rayd 0.7.0 (Configurar AWS). La serie de compatibilidad pasa a 0.7: rayito doctor exige el rayd del tag rayd-v0.7.0 para el SDK 0.7 (Compatibilidad), porque volumes= y el endurecimiento del agente viven en rayd. Contra una imagen anterior, volumes= falla cerrado con UnimplementedError y termina el sandbox que acaba de lanzar.

  3. Redespliega las pilas que uses. rayito events deploy sube el código nuevo de las Lambdas de events-webhooks; redespliega también infra/iam.yaml para tener las políticas separadas (IAM). EventsOperatorPolicy sigue existiendo en 0.7 pero queda obsoleta: vincula a cada identidad EventsLauncherPolicy, EventsReaderPolicy o EventsWebhookAdminPolicy.
  4. Revisa estos cambios de comportamiento:
Qué Antes Desde 0.7.0
rayito sandbox proxy --bind 0.0.0.0 bastaba --allow-remote exige --allowed-host; un Host distinto (p. ej. tras un proxy inverso) recibe 421 sin --allowed-host, un Origin ajeno 403, y una petición con delimitación ambigua 400
.dockerignore de Template.build * cruzaba / * y ? no cruzan /: *.pyc sólo excluye los de la raíz; usa **/*.pyc
Access token propio (access_token=, RAYITO_ACCESS_TOKEN) cualquier longitud al menos 16 bytes decodificados, o InvalidArgumentException / InvalidArgumentError
Checkpoint o restore explícito hacia otra base desde un sandbox con persist= permitido permission_denied
Salida de commands.run y PTY sin tope 64 MiB por descriptor (se conserva el final y truncated lo indica); ajustable con max_output_bytes / maxOutputBytes
Ruta de allow en SecretGateway que rayd nunca dejaría pasar aceptada InvalidArgumentException / InvalidArgumentError antes de llamar a AWS
secrets= con un secreto rayito/webhooks/* aceptado InvalidArgumentException / InvalidArgumentError
register_webhook con una URL inalcanzable (puerto o host inválidos) aceptada InvalidArgumentException / InvalidArgumentError
git con credenciales y url.*.insteadOf en la configuración seguía GitAuthException / GitAuthError

Sin opciones nuevas, el resto de tu código sigue igual: no hay cambios incompatibles en la API pública de 0.6.x fuera de esta tabla.

Lo que no trae 0.7.0

  • Sandbox.create(domain=) y que get_host()/expose() devuelvan la URL del dominio propio: siguen en desarrollo.
  • Las operaciones de contenido del Volume del shim (read_file, write_file… sin sandbox): Rayito no tiene plano de datos fuera de un MicroVM, así que siguen lanzando UnimplementedError.

Detalle completo, cambio a cambio: Changelog.