Secretos¶
Rayito puede guardar secretos en AWS Secrets Manager de tu cuenta
(SecretStore, y el Secret/AsyncSecret del shim de E2B) y entregarlos
a un sandbox como variables de entorno (secrets=), con una caché que evita
traer el secreto en cada llamada (SecretCache).
Desde 0.5.0. Aceptado contra AWS Secrets Manager real.
Coste y activación
- Por defecto: apagado. Sin
secrets=/secret_cache=(TypeScript:secrets/secretCache) y sin instanciarSecretStore, Rayito no construye ningún cliente de Secrets Manager, no importa@aws-sdk/client-secrets-managery no hace ninguna llamada. Ninguna variable de entorno ni fichero de configuración lo enciende. - Activa:
SecretStore(...)guarda, actualiza, lista y borra secretos bajo un prefijo (rayito/);secrets=entrega su valor como variable de entorno de un comando, una PTY, una celda o un contexto. - Recursos y llamadas AWS: un secreto de Secrets Manager por
create;GetSecretValueuna vez por secreto y TTL de la caché (300 s), nunca una vez por comando. - Coste aproximado (us-east-1, consultado 2026-09-30, precios): $0,40 por secreto y mes hasta que lo borras, más $0,05 por 10 000 llamadas (≈ $0,04/mes por secreto y proceso con el TTL por defecto).
- IAM (credenciales de quien llama al SDK):
RayitoSecretsReaderpara inyectar yRayitoSecretsAdminpara el CRUD, deinfra/secrets-access.yaml. - Cómo apagarla: quita
secrets=y deja de instanciarSecretStore;destroy()los secretos que ya no uses (Cómo apagarlo).
Fase 1: el código del sandbox PUEDE leer un secreto inyectado
secrets= entrega el valor como variable de entorno del proceso que lo
pide. Cualquier código que corra en el sandbox puede leerlo: el entorno de
su propio proceso, /proc/<pid>/environ de los procesos del mismo uid
(1000) y el snapshot de memoria de la suspensión (si el sandbox se
suspende con el proceso vivo, el valor queda en ese snapshot; si AWS lo
cifra en reposo es la pregunta abierta SEC-5). La primera vez que un
proceso usa secrets=, el SDK lo recuerda con un RayitoCompatWarning.
Con código no confiable (el de un agente, el de un usuario):
- no inyectes credenciales de larga duración (claves de acceso de IAM, tokens personales sin caducidad);
- inyecta tokens de vida corta y alcance mínimo (por ejemplo, un token de una API con permiso sólo de lectura y que caduque en una hora);
- úsalo sólo en procesos que controlas (el comando concreto que lo necesita, no el shell entero), o
- usa la pasarela de secretos
(
gateways=, desde 0.6.0): el sandbox usa la credencial contra unupstreamfijo sin poder leer su valor.
Qué activa y qué cuesta¶
SecretStore (CRUD) |
secrets= (inyección) |
|
|---|---|---|
| Activa | crear, actualizar, describir, listar y borrar secretos bajo un prefijo (rayito/ por defecto) |
entregar el valor como variable de entorno de un comando, una PTY, una celda Python o un contexto de código |
| Recursos AWS | un secreto de Secrets Manager por create |
ninguno nuevo |
| Llamadas AWS | CreateSecret, PutSecretValue, UpdateSecret, DescribeSecret, ListSecrets, DeleteSecret (una por método; update y destroy hacen antes un DescribeSecret) |
GetSecretValue una vez por secreto y TTL de la caché (300 s por defecto) |
| Coste (us-east-1, precios, consultado 2026-09-30) | $0,40 por secreto y mes, prorrateado, hasta que lo borras, + $0,05 por 10 000 llamadas | $0,05 por 10 000 llamadas: con el TTL por defecto, ≤ 12 lecturas/hora por secreto y proceso ≈ $0,04/mes |
| IAM (credenciales del llamante, no el execution role) | política RayitoSecretsAdmin |
política RayitoSecretsReader |
El log DEBUG de botocore / AWS SDK imprime el valor
Rayito nunca escribe el valor ni el nombre de un secreto en sus logs
(loggers rayito.* en Python, el logger de las opciones en
TypeScript), en repr/toJSON ni en errores. Pero si activas el log
DEBUG del SDK de AWS que Rayito usa por debajo —logging.DEBUG en el
logger raíz o en botocore/urllib3, boto3.set_stream_logger(), o
un logger en el SecretsManagerClient del SDK v3—, ese log
incluye los cuerpos de petición y respuesta de Secrets Manager, con el
SecretString en claro (CreateSecret, PutSecretValue,
GetSecretValue). No lo actives en procesos que manejan secretos, o
limita el nivel DEBUG al logger rayito
(logging.getLogger("rayito").setLevel(logging.DEBUG)).
Contrato exacto de parámetros y errores: AWS_API_NOTES.md §19 (en
GitHub). Una CMK de
KMS (kms_key_id=) añade el coste de KMS y los permisos kms:Decrypt /
kms:GenerateDataKey; la clave gestionada por AWS (aws/secretsmanager) no.
IAM: la plantilla opcional¶
infra/secrets-access.yaml crea dos políticas IAM gestionadas y nada más
(coste $0): RayitoSecretsReader (GetSecretValue, DescribeSecret sobre
arn:aws:secretsmanager:<región>:<cuenta>:secret:rayito/*) y
RayitoSecretsAdmin (además CreateSecret, PutSecretValue,
UpdateSecret, DeleteSecret en ese ARN y ListSecrets en *;
destroy usa DescribeSecret y DeleteSecret, las dos incluidas). Con
KmsKeyArn, añade kms:Decrypt (y kms:GenerateDataKey en la de
administrador) sólo a través de Secrets Manager (kms:ViaService).
RayitoSecretsReader además niega GetSecretValue sobre
rayito/webhooks/*: ahí viven los secretos de firma de los
webhooks, y un sandbox que
recibiera uno por secrets= podría falsificar entregas firmadas. El SDK
también se niega a leerlos por secrets=/SecretCache
(InvalidArgumentException) aunque tu política lo permitiera.
SecretPrefix debe terminar en /: rayito (sin barra) concedería
también secretos ajenos como rayito-prod-db. Se despliega como el
componente secrets-access de rayito stack:
rayito stack deploy secrets-access --param SecretPrefix=rayito/
# con una clave KMS propia (SecretStore(kms_key_id=...)), añade:
# --param KmsKeyArn=arn:aws:kms:<región>:123456789012:key/<id>
rayito stack status secrets-access # salidas ReaderPolicyArn y AdminPolicyArn
Asigna ReaderPolicyArn (o AdminPolicyArn si vas a crear y borrar
secretos) a quien ejecuta el SDK, igual que la política de
Configurar AWS:
aws iam attach-user-policy o aws iam attach-role-policy. SecretPrefix
debe coincidir con SecretStore(prefix=). Para borrarla:
rayito stack destroy secrets-access (quita las políticas, no los
secretos). Desde el SDK, OptionalStacks().deploy("secrets-access", ...)
hace lo mismo. Si prefieres CloudFormation a mano, la plantilla es
infra/secrets-access.yaml; más detalle en
infra/README.md.
Ejemplos¶
import os
from rayito import Sandbox, SecretCache, SecretRef, SecretStore
store = SecretStore(region="us-east-1") # no llama a AWS
store.create("openai", os.environ["OPENAI_API_KEY"], metadata={"team": "ml"})
cache = SecretCache(ttl_seconds=300, region="us-east-1") # opcional
sbx = Sandbox.create(secrets={"OPENAI_API_KEY": "openai"}, secret_cache=cache)
sbx.commands.run("python agent.py") # 0 llamadas: resuelto al crear
sbx.commands.run("python eval.py") # 0 llamadas: acierto de caché
sbx.commands.run("env", secrets={"GH_TOKEN": SecretRef("gh", version_stage="AWSCURRENT")})
sbx.kill()
store.update("openai", os.environ["OPENAI_API_KEY_NUEVA"]) # versión 2
cache.refresh("openai") # sin esperar al TTL
store.destroy("openai") # deja de facturar
import { Sandbox, SecretCache, SecretRef, SecretStore } from "rayito";
const store = new SecretStore({ region: "us-east-1" }); // no llama a AWS
await store.create("openai", process.env.OPENAI_API_KEY!, { metadata: { team: "ml" } });
const secretCache = new SecretCache({ ttlSeconds: 300, region: "us-east-1" });
const sbx = await Sandbox.create({ secrets: { OPENAI_API_KEY: "openai" }, secretCache });
await sbx.commands.run("python agent.py"); // 0 llamadas: resuelto al crear
await sbx.commands.run("env", {
secrets: { GH_TOKEN: new SecretRef("gh", { versionStage: "AWSCURRENT" }) },
});
await sbx.kill();
await store.update("openai", process.env.OPENAI_API_KEY_NUEVA ?? "");
await secretCache.refresh("openai");
await store.destroy("openai");
@aws-sdk/client-secrets-manager es una peerDependency opcional:
instálala (npm install @aws-sdk/client-secrets-manager) sólo si usas
secretos. Sin ella, nada cambia; con secrets o SecretStore y sin
ella, la primera llamada lanza InvalidArgumentError con el comando de
instalación.
import os
from rayito.e2b import Secret
info = Secret.create("openai-key", os.environ["OPENAI_API_KEY"], region="us-east-1")
print(info.secret_id) # el ARN de Secrets Manager
Secret.fill("openai-key") # '${e2b.secrets.openai-key}': NO se resuelve
Secret.destroy("openai-key")
El shim pasa los nombres a minúsculas (como E2B):
Secret.create("OpenAI", …) guarda rayito/openai. secrets= y
SecretStore del SDK nativo no normalizan, así que ese secreto se
inyecta como secrets={"OPENAI_API_KEY": "openai"}; con "OpenAI" la
llamada lanza SecretNotFoundException.
Las diferencias con E2B están en Compatibilidad con E2B.
Dónde se aplica secrets=¶
| Llamada | Python | TypeScript | Notas |
|---|---|---|---|
| Crear / conectar | Sandbox.create(secrets=, secret_cache=), Sandbox.connect(id, secrets=, …), sbx.connect(secrets=, …) |
Sandbox.create({ secrets, secretCache }), Sandbox.connect(id, { secrets }), sbx.connect({ secrets }) |
el handle guarda sólo las referencias; se resuelven antes de run-microvm: un secreto que falta falla sin lanzar un VM; connect sin secrets conserva los del handle (un secret_cache=/secretCache solo cambia la caché, no las referencias); secrets={} (TS: secrets: {}) los borra todos y conserva la caché |
| Pool | pool.take(secrets=), Sandbox.create(pool=, secrets=) |
pool.take({ secrets }), Sandbox.create({ pool, secrets }) |
se enlazan al sandbox que sale del pool; las plazas calientes nunca los llevan (ni en su lanzamiento ni en su SlotRecord) |
| Comandos | commands.run(..., secrets=) (también background=True) |
commands.run(cmd, { secrets }) |
StartRequest.envs |
| PTY | pty.create(secrets=) |
pty.create({ secrets }) |
PtyStart.envs |
| Código | run_code(..., secrets=) |
runCode(code, { secrets }) |
ExecuteRequest.envs, sólo en contextos Python y sólo durante esa celda; con otro language es InvalidArgumentException, y los del handle no se añaden a celdas de otros lenguajes. Con context= dado como id (texto), los del handle sólo van si el contexto es default o uno que ese handle creó o listó como Python: un id desconocido (creado desde otro handle) va sin ellos; pasa el CodeContext o secrets= en la llamada |
| Contextos | create_code_context(secrets=) |
createCodeContext({ secrets }) |
CreateContextRequest.envs: el entorno del kernel, de cualquier lenguaje, mientras viva |
reincarnate() |
relanza con las referencias (y la caché) que tenga el handle en ese momento, incluidas las de pool.take(secrets=) o connect(secrets=) |
igual | sólo referencias, nunca valores |
Reglas:
- Handle ∪ llamada: cada llamada inyecta los secretos del handle más los suyos; si repiten una variable, gana la llamada.
- Todo o nada: una variable que está a la vez en
envsy ensecretsesInvalidArgumentException(TS:InvalidArgumentError), nunca se pisa en silencio. El error nombra la variable, nunca el secreto. - Una cadena simple es
SecretRef(nombre);SecretRef(nombre, version_id=…)oversion_stage=…fija la versión; un ARN completo se usa tal cual. - Nunca viajan en el
runHookPayload(create(envs=)no cambia),metadata, etiquetas, variables de entorno de la imagen, registros del pool, logs,repr/JSON.stringifyni mensajes de error. - La entrega reutiliza los
envsdeProcessService,PtyServiceyCodeService, que ya viajan cifrados hasta el proxy de AWS y autenticados con el access token;raydconstruye el entorno del hijo desde cero y no registraenvs. No hay RPC nueva: funciona con imágenes 0.4.0.
La caché: nunca en cada llamada¶
SecretCache(ttl_seconds=300) (TS: new SecretCache({ ttlSeconds: 300 })):
- Clave: (región, credenciales —la sesión boto3 en Python, el proveedor
de credenciales en TypeScript—,
SecretIdresuelto,VersionIdoVersionStage,AWSCURRENTpor defecto). Dos cuentas o dos regiones nunca comparten un valor. - TTL de 1 a 86 400 s (300 por defecto).
0se rechaza: sería traer el secreto en cada llamada. - Un acierto hace 0 llamadas a AWS. Una sola petición en vuelo por clave:
diez hilos, tareas o promesas a la vez hacen una
GetSecretValue. - Refresco sólo al vencer el TTL o con
refresh(nombre)(lee ya) /invalidate(nombre)(descarta; sin nombre, todo). Tras unSecretStore.update, los lectores ven el valor nuevo al vencer su TTL o conrefresh(). - Un secreto que no existe no se guarda: el siguiente intento vuelve a preguntar.
- Sin
secret_cache=,secrets=usa una caché compartida del proceso por (región, sesión), con TTL 300, creada la primera vez que se usasecrets=. Como mucho hay 32 (MAX_SHARED_CACHES): un proceso que crea una sesión o un proveedor de credenciales por tenant o por petición descarta la usada hace más tiempo (sólo cuesta releer después). - Los valores vencidos se descartan, no se quedan en memoria: cada uso
de una caché (y cada vez que se pide una compartida, para todas las
compartidas) borra antes los valores cuyo TTL ya pasó;
sweep()lo hace a mano. No hay temporizador: una caché que nadie vuelve a usar conserva sus valores hasta que el proceso la suelta. - Los valores viven sólo en la memoria del proceso del SDK;
repr,str,toJSONeinspectmuestran***.
Versiones y metadatos¶
- La versión entera de E2B se codifica en el
ClientRequestTokende Secrets Manager (rayito-secret-version-{n:020d}, 42 caracteres):createes la 1 y cadaupdateescriben + 1, dondenes la mayor versión de Rayito que aún listaDescribeSecret(no sólo laAWSCURRENT: tras una rotación externa la actual no es de Rayito). Dos escritores con valores distintos que calculan el mismon + 1chocan: el segundo recibeSecretExceptiony debe reintentar. Si varias rotaciones externas seguidas dejan las versiones de Rayito sin etiqueta,DescribeSecretya no las lista yupdatepuede repetir un número usado: crea un secreto nuevo en ese caso. updatemás de una vez cada 600 s para el mismo secreto avisa una vez (Secrets Manager recomienda no escribir más de una vez cada 10 minutos y conserva como mucho 100 versiones más las de las últimas 24 h).metadatava enDescriptioncomorayito:v1:+ JSON compacto, ≤ 2048 caracteres (validado antes de llamar a AWS).destroyesDescribeSecrety, si el secreto existe,DeleteSecret(ForceDeleteWithoutRecovery=True): no hay ventana de recuperación. DevuelveTruesi lo borró yFalsesi no existía (o ya estaba programado para borrarse, u otro proceso lo borró a la vez), sin llamar aDeleteSecret, como E2B. ElDescribeSecretprevio es necesario porque AWS acepta el borrado forzado de un nombre que no existe sinResourceNotFoundException.- Recrear un nombre recién borrado: el borrado es asíncrono y AWS tarda
en liberar el nombre (en la aceptación de 0.5.0, entre 19 y 28 s).
Mientras tanto
CreateSecretrespondeInvalidRequestExceptionmencionando el borrado ycreatereintenta con el mismoClientRequestToken, con backoff exponencial (0,5 s doblando hasta 8 s, ±25 % de jitter) durante como mucho 60 s (CREATE_RETRY_BUDGET_SECONDSen Python,CREATE_RETRY_BUDGET_MSen TypeScript). Pasado ese plazo lanzaSecretException(SecretError) conaws_code="InvalidRequestException". list()es eventualmente consistente:ListSecretspuede tardar ~3–5 s en mostrar un secreto recién creado o el cambio de uno recién actualizado (y en dejar de mostrar uno recién borrado).get_info,existsydestroy(DescribeSecret) no tienen ese retraso. Un test que crea y lista debe sondearlist()con un plazo, no comprobarlo una sola vez.
Errores¶
| Python | TypeScript | Cuándo |
|---|---|---|
SecretNotFoundException (también NotFoundException) |
SecretNotFoundError |
el secreto no existe o está programado para borrarse |
SecretException (un SandboxException) |
SecretError (un SandboxError) |
permiso IAM que falta (nombra la acción), ya existe, choque de versión, límite de versiones, secreto binario |
RateLimitException |
RateLimitError |
ThrottlingException tras los reintentos del SDK de AWS |
InvalidArgumentException |
InvalidArgumentError |
nombre, valor (≤ 64 KiB), metadata, TTL o variable inválidos; conflicto envs/secrets; un secreto de firma de webhook (rayito/webhooks/...) en secrets= o SecretCache |
Ningún error contiene el valor ni el nombre del secreto; el mensaje de AWS tampoco se propaga (puede nombrar el secreto): sólo su código. Tampoco los logs de Rayito; el log DEBUG del SDK de AWS sí los contiene (ver el aviso de Qué activa y qué cuesta).
Cómo apagarlo¶
- Quita
secrets=/secret_cache=(TS:secrets/secretCache) de tus llamadas y deja de instanciarSecretStore: Rayito vuelve al camino de 0.4.0. - Borra los secretos que ya no uses (
SecretStore().destroy(nombre),Secret.destroy(nombre)o la consola): Secrets Manager los factura ($0,40/mes cada uno) hasta que se borran, uses o no Rayito. - Si desplegaste la pila de políticas, bórrala:
rayito stack destroy secrets-access.
Modelo de amenazas: T18 de
SECURITY.md
y Seguridad.