Probar en local (Docker + Floci)¶
En el repositorio desde 0.7.0 (Novedades).
Un entorno de Docker para correr los SDK de Python y TypeScript contra un
rayd de verdad y contra un AWS emulado, sin cuenta de AWS, sin credenciales
y sin coste. Sirve para el bucle de desarrollo y para revisar un PR antes de
gastar en la aceptación contra AWS real, que sigue siendo la que cierra un
hito o una release.
Qué levanta¶
┌──────────── red interna "aws" (sin Internet) ────────────┐
│ floci emulador de AWS en :4566 │
│ runner Python (uv) + Node (pnpm), los tests │
│ └─ guest imagen de producto, rayd como PID 1 │
│ (comparte la red del runner: loopback) │
└───────────────────────────────────────────────────────────┘
- floci: Floci 2.1.0 (MIT), fijado por digest. Emula S3, DynamoDB, Secrets Manager, CloudFormation, STS, CloudWatch Logs y el plano de control de Lambda MicroVMs.
- guest: la imagen de
image/Dockerfiletal cual, conraydy el sidecar de kernels. Hace de MicroVM: los tests llaman a sus hooks (/ready,/run,/suspend,/resume,/terminate) como lo haría la plataforma. - runner: las toolchains de los dos SDK. Las dependencias se instalan en
volúmenes, nunca en el
.venvni en losnode_modulesde tu máquina. Corre como uid 993, fuera del rango del sandbox (1000-65535) como el agente de la plataforma:raydrechaza el/runy el/terminateque llegan desde un uid del sandbox. Si levantaste el entorno con una versión anterior y no lo bajaste, corremake local-downantes demake local-uppara que los volúmenes se creen con el dueño nuevo.
Nada publica puertos en tu máquina, ningún contenedor es privilegiado ni
monta el socket de Docker, y Floci no tiene salida a Internet. El porqué de
cada decisión está en docs/research/2026-10-local-testing.md.
Requisitos¶
- Docker con Compose v2 (Docker Desktop, Colima, Lima u OrbStack valen).
- Un host arm64 (Apple Silicon, Graviton o el runner
ubuntu-24.04-armde GitHub): la base de la imagen de producto solo existe para arm64. En x86 funciona con emulación QEMU, mucho más despacio. - Unos 6 GB de disco libres para Docker. De memoria, 3 GB bastan para correr
el entorno, pero compilar
rayddentro de Docker pide unos 6 GB: el crateaws-sdk-s3(montajes S3) solo ya pasa de 2 GB al compilarse. Con menos, compilarayden tu máquina y pásalo conLOCAL_RAYD_BIN(abajo). makeypython3(para copiar el sidecar al contexto del guest).- Sólo para los tests de agentes (abajo):
uvy una sesión de AWS en tu máquina, porquemake local-bedrock-keycorre conuv run.
Uso¶
make local-up # construye y arranca floci, runner y guest; instala dependencias
make local-e2e # corre los tests `local` de Python y de TypeScript
make local-down # lo para todo y borra los volúmenes
make local-up compila rayd dentro de Docker la primera vez (unos minutos,
sin Rust en tu máquina). Si ya tienes el binario de make build (estático,
musl, con cargo zigbuild; funciona también desde macOS), pásalo y te
ahorras la compilación; es la vía si tu VM de Docker tiene menos de 6 GB y
rustc muere con SIGKILL:
Para correr solo una parte, los argumentos de LOCAL_E2E_ARGS van a pytest,
y cada suite se puede lanzar sola dentro del runner:
make local-e2e LOCAL_E2E_ARGS="-k commands"
docker compose -f dev/local/compose.yaml exec runner \
bash -c 'cd clients/python && uv run --no-sync pytest tests/local -m local -k secret'
docker compose -f dev/local/compose.yaml exec runner \
bash -c 'cd clients/typescript && pnpm exec vitest run --project local'
Agentes contra un modelo real¶
Los tests de agentes (test_local_agents.py y agents.local.test.ts)
corren sbx.agent con OpenCode y deepagents
dentro de un sandbox local, con Claude en Amazon Bedrock como modelo y
bedrock_gateway / bedrockGateway como única salida. Comprueban, en cada
runtime y en los dos SDK, que el agente usa sus herramientas, continúa una
sesión, se aborta sin dejar procesos vivos, respeta AgentLimits (pasos,
tokens y timeout), emite el stream de eventos esperado, no puede leer la
credencial, no sale a Internet por su cuenta mientras el modelo sí responde
y que no hay telemetría encendida por defecto. Sin clave se saltan, así que
make local-e2e y CI siguen sin credenciales.
Coste y activación
- Por defecto: apagado. Sin
make local-bedrock-keylos tests de agentes se saltan y nada sale del entorno local. - Activa:
make local-agent-up(cambia el guest por la variante con OpenCode, ripgrep, deepagents y su runner dedev/local/agent/, conCAP_NET_ADMINpara que el deny-all de egress se aplique de verdad) yAWS_PROFILE=<tu-perfil> make local-bedrock-key. - Recursos y llamadas AWS: ningún recurso. La clave es una URL de
bedrock:CallWithBearerTokenprefirmada en tu máquina (vale lo que tu sesión, como mucho 12 h); cada suite hace unas treinta llamadasConverseStream/Conversedesderaydpor la pasarela. El secreto vive en el Secrets Manager de Floci. - Coste aproximado: céntimos por corrida, de tokens de Bedrock (Claude Haiku 4.5, us-east-1, consultado el 2026-10-06: https://aws.amazon.com/bedrock/pricing/).
- IAM:
bedrock:CallWithBearerTokenebedrock:InvokeModel*sobre el perfil de inferencia, y el acceso al modelo ya habilitado en la cuenta (Rayito no lo habilita por ti). - Cómo apagarla:
make local-down(borra el tmpfs del runner con la clave) o no ejecutesmake local-bedrock-key. -
Ejemplo:
La clave pasa del host al runner por una tubería y queda en
/tmp/rayito-local-bedrock-key (RAYITO_LOCAL_BEDROCK_KEY_FILE), un tmpfs
que muere con el contenedor: nunca en un fichero de tu máquina ni en una
variable de entorno. Los tests la buscan dentro del sandbox para decir sí o
no y nunca la imprimen. El modelo y su región se cambian con
RAYITO_E2E_BEDROCK_MODEL y RAYITO_E2E_BEDROCK_REGION (por defecto,
Claude Haiku 4.5 en us-east-1). Un test marcado como fallo esperado
recuerda que el timeout aún no mata lo que lanzó el shell del agente.
Otros proveedores sin claves de terceros¶
test_local_agent_providers.py prueba de verdad los caminos al estilo de
OpenAI de los proveedores del agente con lo
que ya tienes en AWS, sin cuentas en OpenAI ni en otros proveedores:
| Ruta | Pasarela y AgentModel |
Qué comprueba |
|---|---|---|
bedrock-chat |
openai_compatible_gateway hacia bedrock-runtime (/openai/v1), gpt-oss-120b |
herramientas, sesión, egress cerrado, clave ilegible |
mantle-chat |
openai_compatible_gateway hacia bedrock-mantle (/v1) |
lo mismo |
mantle-responses |
provider="openai" (Responses API) hacia bedrock-mantle |
una vuelta sin herramientas con uso de tokens |
litellm |
litellm_gateway hacia un proxy de LiteLLM local delante de Bedrock |
herramientas, sesión, egress cerrado, clave ilegible |
Las tres primeras usan la misma clave de make local-bedrock-key. La de
LiteLLM la crea make local-litellm-up (dev/local/agent/litellm.sh):
arranca LiteLLM, fijado por digest, en la red del entorno con el nombre
litellm y sin puertos en el host; le da un certificado de una CA de
prueba que añade al almacén del sistema del guest (con el que rayd
verifica el upstream de la pasarela; como rayd lo lee al arrancar,
reinicia el guest, así que no lo lances con un sandbox vivo), y deja la
clave maestra en el tmpfs del runner. LiteLLM habla con Bedrock con las credenciales temporales de tu
sesión, que sólo viajan como variables de entorno del contenedor; cuando
caduquen, repite make local-litellm-up.
make local-agent-up
AWS_PROFILE=<tu-perfil> make local-bedrock-key
AWS_PROFILE=<tu-perfil> make local-litellm-up
make local-e2e LOCAL_E2E_ARGS="-k providers"
make local-litellm-down
bedrock-mantle se aparta de la API de OpenAI en dos puntos (medido el
2026-10-07): el response.output_item.done de una llamada a herramienta
llega con id: null y rechaza un mensaje de asistente reenviado sin id
ni status. Por eso mantle-responses sólo prueba una vuelta sin
herramientas; contra la API de OpenAI el camino completo no está probado
de verdad. OpenAI, Gemini, Azure OpenAI, OpenRouter, Groq, Mistral,
DeepSeek y xAI sólo los cubren los tests unitarios contra un upstream falso.
Proveedores de modelo sin claves¶
Los tests de proveedores (test_local_agent_providers_fake.py y
agent-providers.local.test.ts) prueban los nueve presets de
proveedores del agente (OpenAI, Gemini, Azure
OpenAI, OpenRouter, Groq, Mistral, DeepSeek, xAI y LiteLLM) con OpenCode y
con deepagents, sin claves reales ni Internet. make local-providers-up
añade a la variante de agentes un upstream HTTPS falso
(dev/local/providers/fake_upstream.py): en la red interna providers
responde a los nombres de los nueve proveedores, con una CA de un solo uso
en la que sólo confía rayd (SSL_CERT_FILE del guest), y contesta lo
mínimo válido de Chat Completions, Responses y Gemini. Por preset y runtime,
con el egress cerrado, los tests comprueban que:
- el agente completa una vuelta por la pasarela;
- al upstream llega el valor del secreto en la cabecera del preset
(
authorization,x-goog-api-keyoapi-key), y el marcador que ven los runtimes no sale nunca, ni en cabeceras ni en el cuerpo; - sólo llegan rutas de la allowlist, y una ruta fuera de ella recibe 403
de
raydsin llegar al upstream.
CI los corre en local-e2e después de make local-e2e.
Prueba de humo contra las APIs reales¶
test_local_agent_providers_smoke.py (Python) y
agent-providers-smoke.local.test.ts (TypeScript) hacen una vuelta corta de
cada runtime contra la API real de cada proveedor cuya clave exportes, con el
egress del sandbox cerrado: el agente responde con texto y el uso de tokens
de entrada y de salida es mayor que cero. Los dos leen los presets del mismo
catálogo (testdata/agent/provider-catalogue.json), y
make local-providers-smoke corre primero el de Python y después el de
TypeScript. Va sobre make local-agent-up, sin el upstream falso: con él
levantado, los dos se saltan.
Coste y activación
- Por defecto: apagada. Cada preset se salta si no exportas
RAYITO_SMOKE_<PRESET>_SECRET(OPENAI,GEMINI,AZURE_OPENAI,OPENROUTER,GROQ,MISTRAL,DEEPSEEK,XAIoLITELLM). - Activa:
make local-providers-smokecon, por preset, la clave enRAYITO_SMOKE_<PRESET>_SECRET(sinBearer) y el modelo enRAYITO_SMOKE_<PRESET>_MODEL; Azure pide ademásRAYITO_SMOKE_AZURE_OPENAI_RESOURCEy LiteLLMRAYITO_SMOKE_LITELLM_UPSTREAM. Las variables pasan al runner por nombre, nunca por valor en la línea de órdenes. - Recursos y llamadas AWS: ninguno. La clave vive en el Secrets
Manager de Floci;
raydhace por la pasarela una o dos llamadas al proveedor por runtime. - Coste aproximado: cada ejecución lleva
AgentLimits(max_total_tokens=20000)(maxTotalTokens: 20_000en TypeScript) y un prompt sin herramientas. Con un modelo de hasta 0,20 USD por millón de tokens de entrada, los dos runtimes juntos quedan por debajo de 0,01 USD por proveedor y SDK (menos de 0,02 USD con los dos SDK). Elige el modelo más barato de tu cuenta. - IAM: ninguno en AWS. La clave sólo necesita llamar al modelo; ponle un límite de gasto en el proveedor.
- Cómo apagarla: no exportes las variables, o
make local-down. -
Ejemplo:
Cómo funciona por dentro¶
Los tests (clients/python/tests/local/ y clients/typescript/tests/local/)
construyen un LocalGuestControlPlane: otro adaptador del puerto
ControlPlane, que envuelve el plano real apuntado a Floci. run-microvm,
get-microvm, list-microvms y terminate-microvm pasan por el adaptador
de verdad (Floci valida las peticiones contra el modelo del servicio); lo que
Floci no emula lo sustituye el guest:
| Operación | En AWS | En local |
|---|---|---|
| Endpoint del MicroVM | *.lambda-microvm.<región>.on.aws por TLS |
127.0.0.1, h2c por loopback |
| Token del proxy | create-microvm-auth-token |
un valor fijo (lo valida el proxy, no rayd) |
/run |
lo llama la plataforma tras restaurar el snapshot | lo llama el plano local tras /ready |
| Pausa y reanudación | suspend-microvm / resume-microvm |
los hooks /suspend y /resume del guest |
| Terminar | terminate-microvm |
/terminate: rayd sale y Docker lo rearranca |
El SDK publicado no cambia ni sabe que esto existe: los tests le pasan el
plano y el transporte por los parámetros que ya tiene (control_plane= y
transport= en Python, controlPlane y transport en TypeScript). Sin la
variable RAYITO_LOCAL_GUEST, que solo define el runner, los tests local
se saltan.
Qué cubre y qué no¶
| Área | En local | Solo contra AWS real |
|---|---|---|
Comandos, ficheros, PTY, run_code, contextos, connect, métricas |
sí, contra el rayd real |
|
| Pausa y reanudación | la máquina de estados de rayd |
snapshot y restauración de la memoria |
OptionalStacks (rayito stack) |
despliegue, estado, redespliegue y borrado de seis pilas (metadata-index, secrets-access, otlp-export, s3-mounts, sizes-guard, templates), más events-webhooks desde LifecycleEvents |
custom-domain y efs-volumes; permisos IAM y coste reales |
Índice de metadatos, SecretStore, secrets= |
sí | |
Pasarela de secretos (gateways=) |
la sección de ConfigureSandbox y el listener |
el reenvío HTTPS al upstream |
Template.build |
subida del zip a S3 y create-microvm-image |
el build real de la imagen y su /ready |
| Eventos y webhooks | la pila, los webhooks, create(events=) y los tres Lambdas ejecutados en proceso, con una entrega firmada a un receptor local |
la suscripción de CloudWatch Logs y la entrega HTTPS con su filtro SSRF |
| Proxy de AWS (JWE, 403, keepalives) | no | sí |
sbx.agent (OpenCode, deepagents) por la pasarela |
sí, con make local-agent-up y una clave de Bedrock: deny-all de egress real, herramientas, sesión, aborto, límites, eventos |
el primer exec y la memoria en un MicroVM |
Presets de proveedor (openai_gateway…litellm_gateway) |
sí, con make local-providers-up: la cabecera que llega al upstream, el marcador que no sale, el 403 fuera de la allowlist y una vuelta de cada runtime contra un upstream falso |
la respuesta real de cada API (prueba de humo opcional con tu clave) |
| Bloqueo de IMDS, egress, montajes S3 | no: necesitan privilegios en el guest (salvo el egress con make local-agent-up) |
sí |
| Persistencia y transferencias prefirmadas | no: rayd usa IMDS y exige HTTPS |
sí |
| Coste, cuotas, tiempos de arranque | no | sí |
Floci tiene algunas diferencias con AWS que los tests tienen en cuenta (la
versión de un secreto, ListMicrovmImageVersions de una imagen que aún no
existe); están en la investigación.
Seguridad del emulador¶
Floci es código abierto de terceros y su API no tiene autenticación, así que el compose lo trata como no confiable: usuario sin privilegios, sin capabilities, sistema de ficheros de solo lectura, sin socket de Docker, sin Internet y sin puertos en el host. La versión 2.1.0 corrige los avisos críticos publicados hasta la fecha. Antes de subir la versión (Dependabot abre el PR), lee sus notas de release y sus avisos de seguridad.
En CI¶
El workflow local-e2e corre make local-up y make local-e2e en
ubuntu-24.04-arm en los PR que tocan el entorno local o rayd (crates/,
proto/, Cargo.lock), cada noche y a mano
(workflow_dispatch). No usa credenciales ni secretos del repositorio.