Ejecuta cualquier código.
Nada sale de la caja.
Cada ejecución recibe una MicroVM propia: kernel separado, sin red, disco de solo lectura, lista en ~260 ms y destruida al final. ¿Necesitas más? Una máquina persistente con red filtrada por host, credenciales que el código usa sin leer, branch instantáneo y un ejecutable portátil de la máquina entera.
Una máquina nueva por ejecución. Nada queda.
Un contenedor comparte el kernel con tu computadora. Aquí cada ejecución recibe una computadora virtual completa, que solo existe mientras dura.
Por debajo: libkrun como monitor de MicroVMsHypervisor.framework en macOS, KVM en Linuximágenes de los lenguajes a partir de las OCI oficiales, sin Docker
Elige una función y mira la sesión.
Cada escena de abajo es la salida de una VM real, capturada el 20/10/2026 en un Mac M1 Pro por task site:demos. El tiempo a la derecha de cada comando es el medido. Ninguna línea se escribió a mano.
runtimeClassName: rc en k3s y microk8s: cada pod recibe kernel propio y la IP del CNI, con kubectl exec, logs, Service y DNS del clúster. 19 de 19 comprobaciones en los dos.
dockerd en un ext4 de verdad, contenedor anidado, docker.sock en el host.
Vulkan (Venus) hasta Metal: 56,7 tokens/s en la GPU frente a 10,3 en la CPU.
🖥️Pantalla en el navegadorLa pantalla de la máquina en el navegador, con teclado y mouse, desde 127.0.0.1.
Las máquinas se encuentran por db.lab.internal; otra red ni siquiera resuelve.
AWS, R2 o MinIO montado por el guest, lectura bajo demanda.
🔑Agente SSH y secretosgit push con las claves del host, que nunca entran en la VM.
--expose-socket y --mount-socket: socket Unix en ambas direcciones.
Solo biblioteca estándar. Job, máquina, exec y streaming. Go: go get runner.codes/pkg/client.
Captura, clic, teclado y scroll desde la CLI, la API y los SDKs, sin nada instalado en la VM.
🤖Un agente opera la VMrc mcp: Claude Code toma capturas, hace clic y escribe en la pantalla de la máquina.
Lo que se puede montar con esto.
Cada caso de abajo se ejecutó en una VM real, con los comandos que aparecen al lado.
Un agente sin frenos, dentro de una caja.
Claude Code corre dentro de una MicroVM con --dangerously-skip-permissions: edita, instala y ejecuta sin pedir confirmación, porque lo que salga mal se queda en la VM.
- Tu proyecto montado en
/workspace: lo que el agente escribe aparece en tu carpeta. - Terminal en el navegador, solo en
127.0.0.1y con token en la URL. - Cualquier puerto que abra (uvicorn, Vite, una base de datos) se vuelve una pestaña con la app adentro, junto a las pestañas
shellyeventos. - Tus claves SSH por el agente del host:
git pushfunciona, la clave privada nunca entra. - Login una vez: la sesión queda en el disco de la máquina y sobrevive al stop.
rc machine create --name agent --image node:22 --net -e IS_SANDBOX=1 \ --init "npm install -g @anthropic-ai/claude-code" rc machine start --name agent rc machine web-terminal --name agent -- claude --dangerously-skip-permissions
▐▛███▛█ Claude Code v2.1.304 ▝▜██████▀ Opus 5.5 · Claude Max ▝▝ ▝▝ /tmp/demo ❯ Write a tiny Python web server that answers "hello from the playground" on port 8000, start it in the background and check it with curl. Ran 2 shell commands ● curl isn't installed here, so I'll check the server with Python's HTTP client instead. Ran 1 shell command ● The server is running in the background on port 8000 and answers hello from the playground. I couldn't check it with curl because curl isn't installed in this environment. Instead I fetched the page with Python's built-in HTTP client and got 200 text/plain with that text. - Code: /tmp/demo/server.py uses only Python's standard library and answers any GET request. - Log: /tmp/demo/server.log records each request. To check it with curl yourself, install it first (for example ! apt-get install -y curl), then run curl localhost:8000. To stop the server, run pkill -f server.py, or kill its process ID if pkill isn't available either (pgrep wasn't). ✻ Churned for 15s · done 4:18 PM ──────────────────────────────────────────────────────────────────────────────────────────────────── ❯ install curl and check it ──────────────────────────────────────────────────────────────────────────────────────────────────── ⏵⏵ bypass permissions on (shift+tab to cycle)
Una máquina tuya, ahora, en el navegador.
En claude-in-chrome.runner.codes cada visitante recibe una máquina propia con Claude Code abierto y sin confirmaciones de permiso.
- Tu cuenta: el
/logines tuyo y queda solo en tu máquina. - Cualquier puerto que levantes responde en
https://<puerto>-<id>.runner.codes/y se vuelve una pestaña. - Aislada: internet abierta, red privada del proveedor cerrada.
- Temporal: se apaga tras 30 min sin uso y se borra tras 24 h sin uso; como máximo 3 a la vez.
https://8000-<id>.runner.codes/.Ejecuta el código de tus usuarios.
Ejercicios, corrección automática, sandbox de documentación: cada envío corre en una MicroVM nueva, sin red, y desaparece al final.
- 15 lenguajes por la misma ruta.
- Pool ya calentado: 26–43 ms por solicitud.
- Salida en vivo por SSE, y un timeout que mata la VM entera.
rc serve --listen 127.0.0.1:8080 --pool-size 2
curl -s -X POST http://127.0.0.1:8080/api/v1/run \
-H 'content-type: application/json' \
-d '{"language":"python","stdin":"oi","files":[{"name":"main.py","content":"print(input().upper())"}]}'
{"status":"success","stdout":"OI\n","stderr":null,"exception":null,"executionTime":40}Un LLM local, aislado, en la GPU del Mac.
llama.cpp con Vulkan dentro de la VM, hablando con la GPU del host por virtio-gpu (Venus → MoltenVK → Metal).
- 56,7 tokens/s en la GPU frente a 10,3 en la CPU (Qwen2.5 0.5B).
- Sin acceso a tu disco: no se monta ningún directorio del host salvo que declares un volumen.
rc image build --stack llama rc run --stack llama --gpu --code 'vulkaninfo --summary | grep deviceName'
Un escritorio con Claude Code en una pestaña.
XFCE, Firefox y Claude Code en un runner.code, con la pantalla en el navegador. La pantalla sigue a la ventana, y un agente opera la VM con rc mcp: captura, clic y teclado.
- Solo en 127.0.0.1, sin VNC ni cliente extra.
- Start en 5,6 s desde un
rc packdel escritorio listo.
cd examples/desktop-claude && rc machine create --name desk
CLAUDE_CODE_OAUTH_TOKEN=... rc machine start --name desk
claude mcp add rc -- rc mcp --name desk
# la pantalla queda en http://127.0.0.1:6080Tú decides qué alcanza el código.
Una máquina nace sin red y sin nada de tu disco. Cada apertura es una línea declarada. Marca lo que necesitas y mira el runner.code, el comando equivalente y lo que cambia para el código de adentro.
runner.code
lo mismo por la línea de comandos
lo que alcanza el código dentro de la máquina
Para ejecutar código que no escribiste.
El código generado por IA, enviado por un usuario o descargado de internet corre sin acceso a tus archivos, tu red ni tu sistema.
Código generado por IA
Ejecuta lo que escribió el LLM sin revisarlo línea por línea antes. Si sale mal, sale mal dentro de la MicroVM.
Máquina para agentes
Claude Code u opencode con el repositorio montado, red solo hacia la API del modelo, la clave como secreto y el SSH del host. Haz un branch antes de cada intento.
Playgrounds y evaluaciones
Ejecuta código de usuarios en 15 lenguajes por la API, con un pool ya precalentado: 26–43 ms por solicitud.
Más fuerte que un contenedor
Un contenedor solo separa procesos dentro del mismo sistema. La MicroVM es otra computadora: lo que salga mal allí no alcanza la tuya.
Cuándo no usarlo: si el código es tuyo, es confiable y necesita red, un contenedor lo resuelve y es más simple.
| Contenedor | runner.codes | |
|---|---|---|
| Kernel | el mismo del host | propio, uno por máquina |
| Red | encendida por defecto | apagada; abierta host por host, con log de cada rechazo |
| Credenciales | variable legible por el proceso | placeholder; el valor real se inserta en el host, solo para el host permitido |
| Disco | escribible | job: root de solo lectura; máquina: disco propio con branch copy-on-write |
| Fin del job | el proceso muere | la MicroVM entera desaparece |
Un job, una máquina o una API.
Job efímero
MicroVM nueva, ejecuta, devuelve stdout y stderr, y desaparece. La forma más directa de ejecutar un fragmento de código.
- stdout y stderr aparecen mientras el programa corre
rc shellabre un shell interactivo en una MicroVM desechable
# ejecuta y devuelve la salida rc run --stack python --code 'print(2**10)' 1024 # shell interactivo en una MicroVM que desaparece al salir rc shell --stack python
Máquina persistente
Disco escribible que sobrevive entre un start y otro, de una stack o de cualquier imagen OCI. Declarada en un archivo runner.code, versionado junto con el código.
create,start,exec,shell,cp,logs,stop,deletebranch,pause,resume,sync,update(red con la máquina encendida)rc pack create --from-vmla convierte en un ejecutable
rc machine create --name dev --stack python --allow-host pypi.org rc machine start --name dev rc machine exec --name dev -- pip install --user requests rc machine branch --from dev --name tentativa-1 rc machine shell --name tentativa-1 rc machine stop --name dev
API HTTP
Un pool de MicroVMs ya iniciadas por lenguaje, cada una usada una vez: la solicitud tarda 26–43 ms. Salida en streaming (SSE) y las máquinas completas por la API.
- sin autenticación: por eso escucha solo en
127.0.0.1por defecto - un campo desconocido en el JSON se rechaza, no se ignora
rc serve --listen 127.0.0.1:8080 --pool-size 2
curl -s -X POST http://127.0.0.1:8080/api/v1/run \
-H 'content-type: application/json' \
-d '{"language":"python","stdin":"oi","files":[{"name":"main.py","content":"print(input().upper())"}]}'
{"status":"success","stdout":"OI\n","stderr":null,"exception":null,"executionTime":40}Pruébalo tú mismo, paso a paso.
El mismo guion que task runbook ejecuta y verifica solo: 146 verificaciones, 0 fallas en la última ronda. Copia los comandos de cada paso, compara con lo esperado y márcalo. El progreso queda guardado en este navegador.
Secuencia para verificar en una terminal nueva, un comando a la vez, que rc hace lo que promete. Cada paso indica lo que se debe ver.
Este runbook es ejecutable. task runbook (scripts/runbook.sh) ejecuta exactamente estos pasos y verifica cada resultado esperado. Falla si alguno sale distinto. Regla del proyecto: todo recurso nuevo recibe una sección aquí y el paso correspondiente en scripts/runbook.sh, en la misma entrega.
| Última ejecución | Host | Resultado |
|---|---|---|
| 20/10/2026 | macOS 27, Apple M1 Pro | task runbook: 0 fallas en 116 verificaciones (hasta el 8u: usuario, secretos, puentes de socket, ping, staged, rechazos) |
| 19/10/2026 | macOS 27, Apple M1 Pro | task runbook: 0 fallas en 50 verificaciones (con volúmenes y red, pasos 8c y 8d). Prueba de falla: con el DNS del gateway sin filtro, el 8d falló en 2 verificaciones |
Requisitos previos: task krun:install, task build y las imágenes (./bin/rc image build --all; el paso 9 necesita la llama).
Ejecuta con el e2e detenido: las pruebas verifican con pgrep que no quedó ninguna VM, y una máquina tuya encendida las hace fallar.
0Preparar
cd ~/works/labs/github/runner.codes ./bin/rc doctor
Esperado: ok en platform, libkrun, gpu y guest, y una línea ok image <stack> por cada imagen construida.
1Job efímero con salida en streaming
./bin/rc run --stack python --code $'import time\nprint("early")\ntime.sleep(1.5)\nprint("late")'Esperado: early aparece enseguida (~0,3 s, el boot) y late 1,5 s después, mientras el programa aún se ejecuta. Al final, [status=success exit=0 ...] en stderr. La VM se destruye al terminar.
2Crear y encender una máquina
./bin/rc machine create --name teste --stack python ./bin/rc machine start --name teste ./bin/rc machine ls
Esperado:
created teste (stack=python cpus=4 memory=8192MiB disk=10GiB gpu=false);started teste (pid N) in ~200ms;testeconrunningen ells.
La máquina sigue encendida después de que el comando termina.
3Escribir cosas en tres lugares
./bin/rc machine exec --name teste -- sh -c 'echo persistiu > /etc/minha-config; echo dados > /workspace/arquivo.txt; echo some > /tmp/temporario.txt'
Esperado: nada en la salida, y rc 0.
4Detener y encender de nuevo
./bin/rc machine stop --name teste ./bin/rc machine start --name teste
Esperado: stopped teste y started teste (pid N) in ~200ms. El stop hace sync del disco antes de apagar.
5Verificar lo que quedó
./bin/rc machine exec --name teste -- sh -c 'cat /etc/minha-config /workspace/arquivo.txt; cat /tmp/temporario.txt'
Esperado:
persistiu dados cat: can't open '/tmp/temporario.txt': No such file or directory
/etc (capa escribible sobre la imagen) y /workspace (disco de la máquina) persisten; /tmp es tmpfs y vuelve vacío.
6Copiar un archivo a la máquina y de vuelta
head -c 20000000 /dev/urandom > /tmp/rc-up.bin ./bin/rc machine cp /tmp/rc-up.bin teste:/workspace/up.bin ./bin/rc machine exec --name teste -- sha256sum /workspace/up.bin shasum -a 256 /tmp/rc-up.bin ./bin/rc machine cp teste:/workspace/up.bin /tmp/rc-down.bin cmp /tmp/rc-up.bin /tmp/rc-down.bin && echo "idênticos"
Esperado:
uploaded 20000000 bytes ... in ~230ms;- el mismo sha256 dentro y fuera de la máquina;
downloaded ... in ~230ms;idênticos.
7Código de salida y streaming dentro de la máquina
./bin/rc machine exec --name teste -- sh -c 'echo a; sleep 1; echo b; exit 7'; echo "rc=$?"
Esperado: a, b un segundo después y rc=7. El rc de rc es el del comando.
7bTerminal interactiva en la máquina
./bin/rc machine shell --name teste
Dentro del shell que se abra, escribe:
test -t 0 && echo IS-TTY; stty size # IS-TTY e o tamanho do seu terminal sleep 30 # aperte Ctrl+C: interrompe só o sleep echo ALIVE # o shell continua vivo exit 4
Fuera, echo $? muestra 4. La máquina sigue encendida: el shell es una sesión, no la VM.
Esperado: terminal de verdad, con Ctrl+C, flechas, historial, vi y redimensionamiento de la ventana. Es bash si la imagen lo tiene; en la python (Alpine) es sh.
8Logs de la máquina
./bin/rc machine logs --name teste --tail 10
Esperado: las últimas líneas de la consola (el guest registra cada operación). Funciona también con la máquina detenida.
8bMáquina declarada en un runner.code
El runner.code es la máquina declarada en un archivo: stack o imagen, recursos, env, init y workload en un TOML versionable.
mkdir -p /tmp/rc-demo && cd /tmp/rc-demo cat > runner.code <<'TOML' stack = "python" cpus = 2 env = ["GREETING=hello"] workdir = "/app" init = ["echo $(( $(cat count 2>/dev/null || echo 0) + 1 )) > count"] entrypoint = ["sh", "-c"] cmd = ["trap 'echo saved > saved; exit 0' INT; echo \"up $GREETING in $(pwd)\"; while :; do sleep 0.2; done"] stop_signal = "SIGINT" stop_timeout = 5 TOML ~/works/labs/github/runner.codes/bin/rc machine create --name web --cpus 1 cd ~/works/labs/github/runner.codes ./bin/rc machine status --name web --json ./bin/rc machine start --name web ./bin/rc machine logs --name web --source workload ./bin/rc machine exec --name web -- sh -c 'cat count; echo $GREETING; pwd' ./bin/rc machine stop --name web ./bin/rc machine start --name web ./bin/rc machine exec --name web -- sh -c 'cat count saved' ./bin/rc machine stop --name web ./bin/rc machine delete --name web
Esperado:
- el
createsin-fdicefrom runner.code, y el status muestra"cpus": 1: la flag gana sobre el archivo; logs --source workloadmuestraup hello in /app: el workdir fue creado y env y workdir llegaron al workload;- el primer
execimprime1,hello,/app; - el segundo imprime
1ysaved: el init no se ejecutó de nuevo en el segundo start, y elSIGINTdel stop llegó al workload, que escribió el archivo antes del sync.
Una clave que runner.code aún no implementa se rechaza, nunca se ignora:
printf 'stack = "python"\n[health]\nexec = ["true"]\n' > /tmp/rc-demo/bad.code ./bin/rc machine create --name x -f /tmp/rc-demo/bad.code
Esperado: key "health" is not supported yet: health checks, y nada creado.
8cVolúmenes: directorio del host dentro de la máquina
mkdir -p /tmp/rc-vol/ro && echo "do host" > /tmp/rc-vol/hello.txt ./bin/rc machine create --name vol --stack python -v /tmp/rc-vol:/data -v /tmp/rc-vol/ro:/ro:ro -w /data ./bin/rc machine start --name vol ./bin/rc machine exec --name vol -- sh -c 'cat hello.txt; echo do-guest > from-guest.txt; echo y > /ro/y; echo ro=$?' cat /tmp/rc-vol/from-guest.txt ./bin/rc machine stop --name vol ./bin/rc machine delete --name vol
Esperado:
do host: el guest lee el directorio del host;Read-only file systemyro=1: el:rolo impone la libkrun en el device, y el root del guest no lo deshace;- en el host,
catmuestrado-guest: la escritura del guest aparece en el host al instante.
8dRed: salida, política y puerto
La máquina nace sin red. --net habilita la salida; --allow-host la habilita y la restringe; -p publica un puerto en el 127.0.0.1 del host.
./bin/rc machine create --name net --stack python --allow-host example.com -p 18080:8000 ./bin/rc machine start --name net ./bin/rc machine exec --name net -- sh -c 'wget -qO /dev/null http://example.com; echo ok=$?' ./bin/rc machine exec --name net -- sh -c 'wget -qO /dev/null http://iana.org' ./bin/rc machine exec --name net -- sh -c 'wget -qO- -T 3 http://169.254.169.254; echo meta=$?' ./bin/rc machine update --name net --allow-host iana.org ./bin/rc machine exec --name net -- sh -c 'wget -qO /dev/null http://iana.org; echo ok=$?' ./bin/rc machine exec --name net -- sh -c 'cd /tmp && echo do-guest > i.html && (python3 -m http.server 8000 >/dev/null 2>&1 &)' curl -s http://127.0.0.1:18080/i.html cat ~/Library/Caches/runner-codes/machines/net/egress-denials.log ./bin/rc machine stop --name net ./bin/rc machine delete --name net
Esperado:
ok=0paraexample.com;wget: bad address 'iana.org'al instante: el DNS del gateway responde NXDOMAIN, sin esperar timeout;meta=1: la metadata de nube (169.254.169.254) se rechaza incluso con la red abierta;- después del
update,iana.orgpasa sin reiniciar la máquina; curlen el host recibedo-guestpor el puerto publicado;- el log de rechazos tiene
deny dns iana.org: name not in the allow list.
8eMáquina efímera: machine run
./bin/rc machine run --stack python -- python3 -c 'print("ola"); raise SystemExit(3)'; echo "rc=$?"
echo dados | ./bin/rc machine run -i --stack python -- sh -c 'read x; echo leu:$x'
./bin/rc machine run --net --image alpine -- uname -a
./bin/rc machine run -it --image alpine -- /bin/sh # terminal; `exit` sai e apaga a máquina
./bin/rc machine lsEsperado:
olayrc=3: el rc es el del comando;leu:dados: el-ireenvía el stdin (sin-iel stdin no se lee);- el
uname -adel kernel de la libkrunfw en una imagen fuera del catálogo (construida en el primer uso); - el
lssin ningunarun-…: la máquina efímera desaparece al final, incluso con Ctrl+C.
8fUn puerto publicado alcanza un servidor que solo escucha en localhost
./bin/rc machine run -d --name web --stack python -p 18080:9000 -- python3 -m http.server 9000 --bind 127.0.0.1
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:18080/
./bin/rc machine delete --name web -fEsperado: 200. El puerto publicado entra por el guest (op dial en el vsock) y marca el 127.0.0.1 de allí, entonces el servidor que escucha solo en localhost, como vite u openclaw, responde.
8gPantalla en el navegador: display, teclado y mouse
./bin/rc machine create --name tela --image alpine:latest --gpu --net --display 1024x768
./bin/rc machine start --name tela
./bin/rc machine exec --name tela -- sh -c 'apk add -q --no-cache libdrm-tests evtest; cat /sys/class/input/event*/device/name'
./bin/rc machine exec --name tela -- sh -c 'c=$(modetest -M virtio_gpu -c | awk "/connected/ {print \$1; exit}"); (modetest -M virtio_gpu -s $c:1024x768 >/dev/null 2>&1 </dev/null &)'
open http://localhost:6080
./bin/rc machine exec --name tela -- sh -c '(timeout 25 evtest /dev/input/event0 > /tmp/k.log 2>&1 </dev/null &)'
# haz clic en la página y escribe algunas letras; después:
./bin/rc machine exec --name tela -- grep 'value 1' /tmp/k.log
./bin/rc machine delete --name tela -fEsperado:
- el guest lista
runner.code keyboardyrunner.code tablet; - la página muestra las barras de color de
modetest, en vivo, en 1024x768; - cada tecla que se escribe en la página aparece en el
evtestdel guest (KEY_R,KEY_C...).
task runbook verifica el mismo camino sin necesitar ojo humano: el cuadro de 1024x768 llega al visualizador y un KEY_A enviado por POST /input aparece en evtest.
8hPantalla adaptable y control de pantalla
La misma máquina con pantalla del 8g, ahora controlada por la CLI, por la API y por el MCP, y con la resolución que pide la página:
./bin/rc machine create --name tela --image alpine:latest --gpu --net --display 1024x768
./bin/rc machine start --name tela
./bin/rc machine exec --name tela -- sh -c 'apk add -q --no-cache libdrm-tests evtest; mkdir -p /etc/rc; printf "#!/bin/sh\necho \"\$1 \$2\" > /tmp/resize.txt\n" > /etc/rc/display-resize; chmod 755 /etc/rc/display-resize'
curl -s -X POST localhost:6080/resize -d '{"width":900,"height":600}'
./bin/rc machine exec --name tela -- cat /tmp/resize.txt
./bin/rc machine exec --name tela -- sh -c 'c=$(modetest -M virtio_gpu -c | awk "/connected/ {print \$1; exit}"); (sleep 90 | modetest -M virtio_gpu -s $c:1024x768 >/dev/null 2>&1 &)'
./bin/rc machine screen --name tela screenshot -o tela.jpg
./bin/rc machine exec --name tela -- sh -c '(timeout 8 evtest /dev/input/event0 > /tmp/k.log &); (timeout 8 evtest /dev/input/event1 > /tmp/t.log &)'
./bin/rc machine screen --name tela key ctrl+c
./bin/rc machine screen --name tela click 1023 767
./bin/rc machine exec --name tela -- sh -c 'sleep 8; grep -h "value 1" /tmp/k.log /tmp/t.log | head'
./bin/rc machine screen --name tela key hyper
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | ./bin/rc mcp --name tela
./bin/rc machine delete --name tela -fEsperado:
900 600en el/tmp/resize.txt: el tamaño que pide la página llega al hook/etc/rc/display-resizedel guest (en el escritorio, ejecutaxrandr; sin el hook, no pasa nada);screen 1024x768 -> tela.jpg, un JPEG de la pantalla;- en el
evtest:KEY_LEFTCTRLyKEY_Cpresionadas,ABS_Xen32767(el borde derecho) yBTN_LEFT; - la página del visor dibuja una flecha naranja donde la CLI hizo clic (
GET /pointerregistra elclick); - la página de la pantalla muestra
runner.codesen la barra (display_titleen elrunner.codecambia el nombre); key hypersale con error (unknown key "hyper"), sin enviar nada a la máquina;- el
rc mcpresponde alinitializey lista 9 herramientas:screenshot,click,move,drag,type,key,scroll,actionsyexec.
task runbook hace lo mismo y verifica también la API: GET /api/v1/machines/:name/screen con X-Screen-Width: 1024, 400 para una acción inválida y 204 para una válida en POST .../screen/actions.
8iBranch y pausa
./bin/rc machine create --name mom --stack python && ./bin/rc machine start --name mom ./bin/rc machine exec --name mom -- sh -c 'echo antes > /root/marca' ./bin/rc machine pause --name mom && ./bin/rc machine status --name mom ./bin/rc machine branch --from mom --name kid ./bin/rc machine resume --name mom ./bin/rc machine start --name kid && ./bin/rc machine exec --name kid -- cat /root/marca ./bin/rc machine delete --name kid -f && ./bin/rc machine delete --name mom -f
Esperado:
statusmuestrapaused, y unexecen ese estado se rechaza con el motivo;- la hija muestra
antes: el branch lleva lo que la madre escribió, incluso con ella pausada (rcla reanuda por un instante para elsync); - después de eso, madre e hija son independientes.
8jRed con nombre
./bin/rc machine run -d --name db --stack python --network lab -- python3 -m http.server 8080 ./bin/rc machine create --name app --stack python --network lab && ./bin/rc machine start --name app ./bin/rc machine exec --name app -- wget -qO- -T 3 http://100.98.0.10:8080/ ./bin/rc machine exec --name app -- wget -qO- -T 3 http://db.lab.internal:8080/ ./bin/rc machine delete --name db -f ./bin/rc machine exec --name app -- nslookup db.lab.internal ./bin/rc machine delete --name app -f
Esperado:
- las dos máquinas reciben IPs consecutivas de la red (
dbes100.98.0.10,appes100.98.0.11) y se alcanzan por IP y por el nombre<máquina>.<rede>.internal, sin--net: el tráfico dentro de la red no es salida; - una máquina de otra red no alcanza estas, ni resuelve sus nombres;
dbborrada desaparece del DNS de la red: elnslookupya no devuelve su IP.
8kVolumen S3
Un S3 local, sirviendo una carpeta (MinIO salió de nixpkgs el 11/10/2026):
mkdir -p /tmp/s3/bkt/data && echo 'ola do bucket' > /tmp/s3/bkt/data/hello.txt nix shell nixpkgs#rclone -c rclone serve s3 /tmp/s3 --addr 127.0.0.1:19000 --auth-key chave,segredo123 & ./bin/rc machine create --name s3 --stack python --net --allow-cidr 127.0.0.1/32 \ -e AWS_ACCESS_KEY_ID=chave -e AWS_SECRET_ACCESS_KEY=segredo123 -e AWS_ENDPOINT_URL=http://100.96.0.1:19000 \ -v s3://bkt/data:/data ./bin/rc machine start --name s3 ./bin/rc machine exec --name s3 -- sh -c 'cat /data/hello.txt; echo do-guest > /data/novo.txt' cat /tmp/s3/bkt/data/novo.txt ./bin/rc machine delete --name s3 -f
Esperado:
- el
startmonta el bucket (con una clave incorrecta, falla cons3: 403y el motivo); - el guest lee
ola do bucket, ynovo.txtaparece en la carpeta del servidor condo-guest.
8lCredencial que la máquina usa sin leer
./bin/rc machine create --name cred --stack python --credential tok=RB_TOKEN@postman-echo.com
RB_TOKEN=valor-do-runbook ./bin/rc machine start --name cred
./bin/rc machine exec --name cred -- python3 -c '
import json, os, urllib.request
tok = os.environ["RB_TOKEN"]
print("guest:", tok)
r = urllib.request.Request("https://postman-echo.com/headers", headers={"Authorization": "Bearer " + tok, "User-Agent": "runbook/1"})
print("server:", json.load(urllib.request.urlopen(r, timeout=20))["headers"]["authorization"])'
./bin/rc machine delete --name cred -fEsperado:
guest: RC_PLACEHOLDER_TOK_...: el guest solo tiene el placeholder;server: Bearer valor-do-runbook: el servidor recibió el valor real, que vino del entorno delstart.
El User-Agent es propio porque el Cloudflare de postman-echo rechaza Python-urllib (403, error code 1010).
8mPack: la máquina en un archivo
./bin/rc machine create --name pk --stack python && ./bin/rc machine start --name pk ./bin/rc machine exec --name pk -- sh -c 'echo marca-do-runbook > /root/marca' ./bin/rc machine stop --name pk ./bin/rc pack create --from-vm pk --output /tmp/one --single-file ./bin/rc machine delete --name pk /tmp/one run -- cat /root/marca /tmp/one
Esperado:
marca-do-runbook: el artefacto llevó el disco de la máquina, incluso con el origen borrado (un pack que perdiera el disco aún arrancaría y saldría con 0; por eso se verifica un valor);/tmp/onesolo muestrausage: one run ....
8nUsuario, volumen de disco y docker.sock
./bin/rc machine create --name usr --stack python --user nobody
./bin/rc machine start --name usr
./bin/rc machine exec --name usr -- id -un
./bin/rc machine exec --name usr --user root -- id -un
./bin/rc machine create --name dk --stack python --docker-socket -v /var/lib/dados -- python3 -c '
import os, socket
s = socket.socket(socket.AF_UNIX); s.bind("/var/run/docker.sock"); s.listen()
while True:
c, _ = s.accept(); c.sendall(b"do-guest:" + c.recv(64)); c.close()'
./bin/rc machine start --name dk
./bin/rc machine exec --name dk -- stat -f -c %T /var/lib/dados
python3 -c 'import socket,sys; s=socket.socket(socket.AF_UNIX); s.connect(sys.argv[1]); s.sendall(b"ping"); print(s.recv(64).decode())' "$(./bin/rc machine data-dir --name dk)/docker.sock"Esperado:
nobody, yrootcon--user root(el--userdel exec gana sobre el de la máquina);ext2/ext3: el volumen de disco es ext4 de verdad, fuera del overlay (lo que necesita el overlay2 de Docker);do-guest:ping: el host habla con el servidor del guest por eldocker.sockdel directorio de la máquina.
8oSecretos y agente SSH
ssh-keygen -q -t ed25519 -N "" -f /tmp/rb-id && ssh-agent -D -a /tmp/rb-agent.sock & sleep 1 SSH_AUTH_SOCK=/tmp/rb-agent.sock ssh-add -q /tmp/rb-id echo do-arquivo > /tmp/rb-sec.txt ./bin/rc machine create --name sec --image alpine --net --init 'apk add -q --no-cache openssh-client' \ --secret-env API=RB_SECRET --secret-file ARQ=/tmp/rb-sec.txt --ssh-agent RB_SECRET=valor-secreto SSH_AUTH_SOCK=/tmp/rb-agent.sock ./bin/rc machine start --name sec ./bin/rc machine exec --name sec -- sh -c 'echo $API $ARQ; ssh-add -l' grep -rl valor-secreto --exclude='*.ext4' "$(./bin/rc machine data-dir --name sec)"
Esperado:
valor-secreto do-arquivo: los dos secretos en el entorno de todo comando;ssh-add -llista la clave de/tmp/rb-id(la privada queda en el agente del host);- el
grepno encuentra nada: el valor no queda en el registro de la máquina.
El agente y la clave son desechables, de este paso: nada toca el agente de quien ejecuta.
8pPuentes de socket en ambas direcciones
python3 -c 'import socket; s=socket.socket(socket.AF_UNIX); s.bind("/tmp/rb-h.sock"); s.listen()
while True:
c,_=s.accept(); c.sendall(b"do-host:"+c.recv(64)); c.close()' &
./bin/rc machine create --name skt --stack python --expose-socket /run/app.sock --mount-socket /tmp/rb-h.sock:/run/h/h.sock -- python3 -c '
import socket
s = socket.socket(socket.AF_UNIX); s.bind("/run/app.sock"); s.listen()
while True:
c, _ = s.accept(); c.sendall(b"do-guest:" + c.recv(64)); c.close()'
./bin/rc machine start --name skt
python3 -c 'import socket,sys; s=socket.socket(socket.AF_UNIX); s.connect(sys.argv[1]); s.sendall(b"ping"); print(s.recv(64).decode())' "$(./bin/rc machine data-dir --name skt)/app.sock"
./bin/rc machine exec --name skt -- python3 -c 'import socket; s=socket.socket(socket.AF_UNIX); s.connect("/run/h/h.sock"); s.sendall(b"pong"); print(s.recv(64).decode())'Esperado: do-guest:ping (el host alcanza el socket en el que escucha el workload) y do-host:pong (el guest alcanza el socket del host, en un directorio que la imagen no tenía).
8qPing y rango de puertos
./bin/rc machine create --name png --stack python --net -p 18301-18303:18301-18303 -- python3 -m http.server 18302
./bin/rc machine start --name png
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:18302/
./bin/rc machine exec --name png -- ping -c1 -W3 1.1.1.1
./bin/rc machine create --name pngd --stack python --allow-cidr 198.51.100.0/24
./bin/rc machine start --name pngd
./bin/rc machine exec --name pngd -- ping -c1 -W2 1.1.1.1
grep 'deny icmp' "$(./bin/rc machine data-dir --name pngd)/egress-denials.log"Esperado:
200: el rango publica un puerto por número (hasta 64), y el del medio responde;- el ping a
1.1.1.1vuelve con la red abierta, y solo si el host recibió la respuesta (no hay respuesta inventada); - fuera de la allowlist el ping no vuelve, y la denegación
deny icmp 1.1.1.1va al log.
8rVolumen staged y sync
mkdir -p /tmp/rb-repo && echo original > /tmp/rb-repo/a.txt && echo x > /tmp/rb-repo/old.txt ./bin/rc machine create --name stg --stack python -v /tmp/rb-repo:/work:staged ./bin/rc machine start --name stg ./bin/rc machine exec --name stg -- sh -c 'stat -f -c %T /work; echo mudou > /work/a.txt; rm /work/old.txt; printf "#!/bin/sh\n" > /work/run.sh; chmod 755 /work/run.sh' cat /tmp/rb-repo/a.txt ./bin/rc machine sync --name stg cat /tmp/rb-repo/a.txt; ls /tmp/rb-repo; ls -l /tmp/rb-repo/run.sh
Esperado:
ext2/ext3: la copia está en el disco de la máquina, sin ida y vuelta de virtio-fs;- antes del sync el host sigue
original; - después:
mudou, sinold.txt(el sync espeja: lo que desapareció de la copia desaparece del host) yrun.shcon-rwxr-xr-x.
El stop ordenado y el final de machine run también sincronizan; --timeout 0 no. La hija de un branch no devuelve nada al host.
8sRechazos que dicen el motivo
./bin/rc doctor | grep -E 'cuda|nested' ./bin/rc machine create --name x --image alpine --cuda time ./bin/rc machine run --nested --image alpine -- ls -l /dev/kvm
Esperado:
info cuda: not supported (no CUDA remoting; --gpu is Vulkan)y la línea de nested de este host;--cuda is not supported: rc has no CUDA remoting ..., sin crear máquina;- en un host que no anida (M1/M2),
failed to start: Error: nested virtualization is not supported on this host ...en menos de 1 s (antes: 30 s ydid not become ready); en un host que anida,/dev/kvmdentro de la VM.
8tDirectorio de la máquina y limpieza de la caché
ls "$(./bin/rc machine data-dir --name usr)" ./bin/rc image prune ./bin/rc pack prune
Esperado: machine.json en el directorio de la máquina; los dos prunes salen con 0 (sin --all, image prune solo borra el ext4 al que ya no apunta ningún nombre, el build anterior de una imagen, y pack prune conserva los 5 usados más recientemente).
8uLas guías
task guides DIRS=gpu-cuda # uma pasta; sem DIRS, todas (cada uma liga as suas VMs)
Esperado: falhas: 0 en cada carpeta. Lo que prueba cada carpeta está en examples/guides/README.md.
8vTerminal web
./bin/rc machine web-terminal --name demo # abre el navegador con el shell de la máquina ./bin/rc machine web-terminal --name demo --no-open -- sh -c 'echo wt-ok:$((6*7))'
Esperado:
web terminal: http://127.0.0.1:PUERTO/?t=<32 hex>: solo loopback, con token; el navegador se abre solo (--no-opensolo imprime);- en el navegador, un terminal real (xterm.js): Ctrl+C, flechas, editores a pantalla completa y el tamaño de la ventana llegan al guest, y un F5 vuelve a mostrar la pantalla reciente;
- con el comando, la pantalla muestra
wt-ok:42, la página avisa que el programa salió yrcsale con su código; /streamsin el token responde 403, y unHostque no sea de loopback también (DNS rebinding).- pestañas
claude(el comando),shell(un terminal aparte, con el primer clic),eventosy, al final, una pestaña por puerto: un servidor que el programa levante en la VM (cualquier puerto) aparece con la app en un iframe y responde en el127.0.0.1del host.
Para abrir Claude Code en el navegador así: rc machine web-terminal --name agent -- claude --dangerously-skip-permissions.
8wPlayground público (claude-in-chrome.runner.codes)
Cada visitante recibe una Fly Machine propia (Fly no expone /dev/kvm, así que la microVM es la de Fly), con Claude Code en el terminal web y los puertos en https://<puerto>-<id>.runner.codes/. Máximo 3 máquinas; 30 min sin uso la apaga, 24 h sin uso la borra.
scripts/playground-cloudflare.sh # túnel claude-in-chrome, ingress y CNAMEs (idempotente) scripts/playground-fly.sh setup # apps runner-codes-playground y runner-codes-visitors, red propia, secrets scripts/playground-image.sh all # imágenes visitor y front con Nix en el builder de Fly scripts/playground-fly.sh deploy # publica el front scripts/playground-token-check.sh # el token del front solo alcanza la app de visitantes curl -s https://claude-in-chrome.runner.codes/ | grep -c FingerprintJS.load
Esperado:
- la página abre, calcula el visitorId del navegador y lleva al terminal
/v/<id>/?t=<token>con Claude Code; /api/sessioncon visitante inválido responde 400; con 3 máquinas en uso, 503 "playground is full";https://3000-0000000000.runner.codes/(máquina que no existe) responde 502, ydocs.runner.codessigue en 200;scripts/playground-token-check.sh: 200 en la app de visitantes, 4xx en el sitio y en el propio front;- dentro de la máquina del visitante, abrir conexión a la red privada de Fly (
_api.internal) falla, y DNS e internet funcionan (scripts/fly-firewall-probe.shmide el mismo firewall en una máquina desechable).
8xRuntime de Kubernetes (k3s y microk8s)
Cada pod con runtimeClassName: rc se vuelve una microVM de libkrun con kernel propio: containerd llama a containerd-shim-rc-v2, el contenedor pause arranca la VM y los contenedores de la app corren dentro de ella. La VM toma la IP que el CNI le dio al pod (un TAP en el netns del pod con tc mirred). Necesita un nodo Linux con /dev/kvm; los de prueba son droplets de DigitalOcean (task k8s:kvm-probe lo mide).
K8S_NODE=k3s task k8s:node -- create && K8S_NODE=k3s task k8s:node -- provision # igual con K8S_NODE=microk8s # en el nodo: curl -sfL https://get.k3s.io | sh - o snap install microk8s --classic task k8s:e2e -- k3s # lleva el árbol, compila, corre deploy/k3s/install.sh y deploy/k8s/e2e.sh k3s task k8s:e2e -- microk8s # lo mismo con deploy/microk8s/install.sh task k8s:defects # planta 20 defectos en el código del runtime y comprueba que los tests los atrapan
Esperado (19 comprobaciones de deploy/k8s/e2e.sh, falhas: 0 en los dos):
rc-helloqueda Ready y su log muestraRC_K8S_OK, el kernel de la VM (6.12.91, no el del nodo) y la IP del pod eneth0;kubectl execsin TTY, con TTY y con stdin; el código de salida del exec (7) y del contenedor (3) vuelven bien;- DNS del clúster, pod rc ↔ pod runc en los dos sentidos y un Service ClusterIP hasta el pod rc;
- dos contenedores en un pod: la misma VM (mismo
boot_id) y el mismo emptyDir; - el resumen de estadísticas del kubelet responde (el uso de la VM va al primer contenedor);
- tras el delete, no queda ningún directorio en
/var/lib/rc-shim/podsni ningún procesorc vmm.
Límites declarados: port-forward, hostNetwork, hostIPC y la propagación de montajes no cruzan la VM; los volúmenes se copian al share del pod al crear el contenedor (lo que el kubelet cambie después, como el token de serviceaccount, no llega); los límites de CPU/memoria valen para la VM entera (anotaciones runner.codes/cpus y runner.codes/memory-mib del pod); el vmm del pod corre sin Landlock/seccomp.
9GPU
Solo si la imagen llama está construida (rc doctor muestra ok image llama).
./bin/rc run --stack llama --gpu --code 'vulkaninfo --summary | grep deviceName' ./bin/rc run --stack llama --gpu --timeout 120s --code 'llama-bench -m /opt/models/qwen2.5-0.5b-q4_k_m.gguf -p 128 -n 32 -ngl 99 -dev Vulkan0'
Esperado:
deviceName = Virtio-GPU Venus (Apple M1 Pro), y también una línea dellvmpipe(Vulkan en software);- la tabla de
llama-benchconVulkan0,pp128en el rango de 900–1.365 t/s ytg32de ~57 t/s.
Detalles en GPU.
10API HTTP
En una terminal:
./bin/rc serve --pool-size 0
En otra:
curl -s -X POST localhost:8080/api/v1/machines/teste/exec -H 'content-type: application/json' -d '{"command":["python3","-c","print(6*7)"]}'
curl -s localhost:8080/api/v1/machines | head -c 300; echoEsperado:
{"exitCode":0,"stdout":"42\n","stderr":"",...};- la lista con
"name":"teste"y"state":"running".
Los cuerpos alternativos y el stream por línea, en el mismo servidor:
curl -s -o /dev/null -w '%{http_code}\n' -X POST localhost:8080/api/v1/machines -H 'content-type: application/json' -d '{"name":"t2","image":"alpine","allowedHost":["a.com"]}'
curl -s -X POST localhost:8080/api/v1/machines -H 'content-type: application/json' -d '{"name":"t3","image":"alpine","allowedHosts":["api.example.com"],"memoryMb":512}'
curl -s -o /dev/null -w '%{http_code}\n' -X POST localhost:8080/api/v1/machines -H 'content-type: application/json' -d '{"name":"t4","image":"alpine","nestedVirt":true}'
curl -sN -X POST localhost:8080/api/v1/machines/teste/exec/stream -H 'content-type: application/json' -d '{"command":["sh","-c","echo a; echo b"]}'
curl -s -o /dev/null -w '%{http_code}\n' -X POST localhost:8080/api/v1/machines/teste/syncEsperado:
400: un campo desconocido (allowedHost, con error de tipeo) se rechaza, y no se ignora dejando la salida abierta;"allow_hosts":["api.example.com"],"enabled":truey"memory_mib":512: los nombres alternativos valen;400ennestedVirt(solo pasa conrc serve --allow-nested-virt);event: stdout/data: a, despuésdata: b: una línea por evento, texto crudo, y elevent: exital final;204en elsync(máquina sin volumen staged: nada que hacer).
Ctrl+C termina el serve. La máquina sigue encendida, porque está desacoplada.
10bSDKs Go y Python
Con el serve todavía en marcha:
go run ./examples/sdk-go http://127.0.0.1:8080 PYTHONPATH=sdk/python python3 examples/sdk-python/main.py http://127.0.0.1:8080
Esperado, en ambos:
job: 42(job efímero);machine: sdk-... runningyexec: ola do sdk go(opython): máquina creada, archivo escrito y ejecutado;error: NOT_FOUND: el rechazo de la API llega como error tipado, con el código estable;- no queda ninguna máquina
sdk-*en./bin/rc machine ls.
10cSDK Go en un proyecto externo (go get)
El módulo Go se llama runner.codes, y https://runner.codes responde al go get con la meta go-import que apunta a GitHub. En un directorio fuera del repositorio, con el serve todavía en marcha:
curl -s 'https://runner.codes/pkg/client?go-get=1' mkdir /tmp/sdkcheck && cp examples/sdk-go/main.go /tmp/sdkcheck/ && cd /tmp/sdkcheck go mod init sdkcheck go get runner.codes/pkg/client@latest go run . http://127.0.0.1:8080
Esperado:
- el
curldevuelve<meta name="go-import" content="runner.codes git https://github.com/andrebassi/runner-codes">; - el
go getdescargarunner.codes v1.1.0(o más nueva); - la salida del
go runes la misma del 10b:job: 42,exec: ola do sdk goyerror: NOT_FOUND.
Lo mismo para Python, en un venv limpio, instalando desde GitHub:
python3 -m venv /tmp/sdkcheck/venv /tmp/sdkcheck/venv/bin/pip install "git+https://github.com/andrebassi/runner-codes@v1.2.0#subdirectory=sdk/python" /tmp/sdkcheck/venv/bin/python -I <repositorio>/examples/sdk-python/main.py http://127.0.0.1:8080
Esperado: el pip instala runner-code 1.1.1, y la salida es la misma del 10b (job: 42, exec: ola do sdk python, error: NOT_FOUND). El -I garantiza que el paquete viene del venv, no del repositorio.
10dRecetas de los SDKs
Las siete recetas de la página de SDKs, en Go y en Python, con el serve todavía en marcha:
go run ./examples/sdk-go-recipes http://127.0.0.1:8080 PYTHONPATH=sdk/python python3 examples/sdk-python-recipes/main.py http://127.0.0.1:8080
Esperado, en ambos (el tiempo varía):
untrusted: stdout=45 stderr=aviso tempo=58ms files: 42 timeout: exception=Timeout compile: exception=Compilation failed stderr_tem_erro=true machine: sdk-go pypi=200 example=bloqueado branch: feito antes do branch errors: 404 NOT_FOUND
En la línea machine, sdk-python en lugar de sdk-go en el segundo programa. No queda ninguna máquina recipe-* en ./bin/rc machine ls.
10eEscritorio con Claude Code por el SDK Go
Con el serve todavía en marcha, la máquina del examples/desktop-claude/runner.code (XFCE, Firefox, Claude Code), creada y verificada por el SDK Go:
go run ./examples/desktop-claude -server http://127.0.0.1:8080 -name desk -display-port 6081 open http://127.0.0.1:6081
Esperado, en la salida:
claude 2.1.x (Claude Code)ynavegador padrão /usr/lib/firefox-esr/firefox-esr;atalhos Arquivos.desktop Claude.desktop Navegador.desktop Terminal.desktop;tela responde ... current 1200 x 700: después de 200 movimientos de mouse, la pantalla sigue en pie y en el tamaño pedido;terminal maximizado _NET_WM_STATE_MAXIMIZED_HORZ;sessão de volta em 1200x700: el programa cierra la sesión XFCE (como un logout) y vuelve sola.
En el navegador: fondo negro, los cuatro accesos directos, y la pantalla del tamaño de la ventana.
10fTareas al Claude Code de la máquina
Necesita el Claude con sesión iniciada en la máquina: el token entra por la credencial del runner.code (el guest solo ve un placeholder), desde el entorno de quien hizo el start. Con el serve iniciado con el token en el entorno:
CLAUDE_CODE_OAUTH_TOKEN="$(pass show bassi/anthropic/claude-code-token)" ./bin/rc serve --listen 127.0.0.1:8080 & go run ./examples/desktop-claude -server http://127.0.0.1:8080 -name desk -display-port 6081 go run ./examples/desktop-claude-agent -server http://127.0.0.1:8080 -name desk
Esperado: ✓ /root/agent/headless.txt, ✓ /root/agent/gui.txt y ✓ /root/agent/keys.txt, cada uno con feito pelo modo <modo>. Son tres formas de enviar la tarea: claude -p por exec, escrita en la ventana con xdotool, y escrita por la API de pantalla. Las pantallas de las dos últimas quedan en agent-gui.jpg y agent-keys.jpg.
task runbook solo ejecuta este paso con RB_CLAUDE=1 y el token en el entorno, porque usa la suscripción de Claude.
11Limpiar
./bin/rc machine stop --name teste ./bin/rc machine delete --name teste rm -rf /tmp/rc-up.bin /tmp/rc-down.bin /tmp/rc-demo /tmp/rc-vol
Esperado: stopped teste y deleted teste. ./bin/rc machine ls vuelve vacío.
extraCuando algo salga distinto
| Síntoma | Dónde mirar |
|---|---|
image for X not built | ./bin/rc image build --stack X |
read stream: the guest in this image does not support streaming | imagen de antes del streaming: reconstruir |
INVALID_STATE: machine X is not running | ./bin/rc machine start --name X |
CONFLICT: machine X already exists | otro nombre, o delete antes |
start falla con el final de la consola | ./bin/rc machine logs --name X |
GPU: ERROR_OUT_OF_HOST_MEMORY | GPU › Por qué Fedora |
| máquina que no se detiene | ./bin/rc machine stop --name X --timeout 0 la mata al instante |
Fuente: docs/runbook.md · ejecutable: scripts/runbook.sh
Nada aquí es promesa.
Cada función tiene test unitario, una prueba en una MicroVM real y defectos plantados a propósito para comprobar que los tests fallan.
Planté 28 defectos en las funciones nuevas (red, ICMP, staged, sockets, nested, SSE…); los tests atraparon los 28. Ronda del 20/10/2026, macOS 27, M1 Pro.
Lo bastante rápido para no usar contenedor.
Arranque medido el 19/10/2026 (30 arranques) y branch el 20/10/2026, en un Mac con Apple Silicon. El informe completo está en la documentación.
15 en el catálogo, y un LLM.
Cada uno con imagen, techo de CPU y RAM y precalentamiento propios, en un YAML. Las máquinas aceptan cualquier imagen OCI (redis:7-alpine, postgres:16-alpine…).
- Branch, pausa y checkpoint de disco listo
- Red por host, CIDR y red con nombre, ping incluido listo
- Credenciales sin leer, secretos y agente SSH listo
- Pack portátil, volumen staged y bucket S3 listo
--nested(KVM dentro de la VM) solo se midió el rechazo
--nested exige un Mac M3 o más nuevo, o Linux con KVM anidado. En este M1 lo rechaza al instante, con el motivo.
Mac y Linux, sin Docker.
runner.codes usa el hypervisor que el sistema ya tiene. Sin Docker, sin máquina en la nube.
Apple Silicon, mediante Hypervisor.framework. Es donde se midieron los números de esta página.
x86_64 y aarch64, mediante KVM (tu usuario necesita acceso a /dev/kvm). El proceso de cada MicroVM corre con Landlock y seccomp. El arranque en Linux con KVM de verdad todavía no se midió.
libkrun no tiene backend Hyper-V. El camino es WSL2 con virtualización anidada, donde corre el rc de Linux.
Listos para correr, probados en una VM.
Cada ejemplo arranca en una VM real, y cada guía tiene un run.sh que prueba lo que promete.
Tres comandos, todo con Nix.
Sin Homebrew y sin Docker. La primera instalación en macOS compila libkrun (unos 10 min); en Linux viene lista de nixpkgs y tu usuario necesita acceso a /dev/kvm.
./bin/rc doctor comprueba si el host está listo.
git clone https://github.com/andrebassi/runner-codes cd runner-codes task krun:install # libkrun + libkrunfw task build # host + guest ./bin/rc image build --stack python # 8–10 s ./bin/rc run --stack python --code 'print("olá da MicroVM")'
Documentación que un agente lee de una vez.
Cada comando, flag, ruta y error, en una sola página.
runner-code.mdEl archivo de la máquina, clave por clave.
network.mdPolítica de salida, puertos, volúmenes, S3, red con nombre.
credentials.mdCredenciales que la máquina usa sin leer.
pack.mdLa máquina en un ejecutable.
architecture.mdCómo conversan el host, el VMM y el guest.
runbook.mdEl paso a paso para probar a mano.
languages.yamlEl catálogo de lenguajes, con imagen y límites.