运行任何代码。
什么都出不了盒子。
每次执行都有自己的 MicroVM:独立内核、无网络、只读磁盘, 约 260 ms 就绪,结束即销毁。需要更多?一台持久化机器:网络按 host 过滤, 代码能用却读不到的凭据、瞬时 branch,以及整台机器的可移植可执行文件。
每次执行一台新机器。什么都不留下。
容器和你的电脑共用内核。这里每次执行都有一台完整的虚拟电脑,只在这次执行期间存在。
底层:libkrun 作为 MicroVM 监视器macOS 上用 Hypervisor.framework,Linux 上用 KVM语言镜像来自官方 OCI 镜像,无需 Docker
选一个功能,看它的会话。
下面每个场景都是真实 VM 的输出,于 2026-10-20 在一台 M1 Pro Mac 上由 task site:demos 采集。每条命令右侧的时间是实测值。没有一行是手写的。
在 k3s 和 microk8s 上使用 runtimeClassName: rc:每个 pod 拥有独立内核和 CNI 分配的 IP,支持 kubectl exec、日志、Service 和集群 DNS。两者均通过 19 项检查。
dockerd 运行在真正的 ext4 上,嵌套容器,docker.sock 暴露到 host。
Vulkan(Venus)一路到 Metal:GPU 上 56.7 tokens/s,CPU 上 10.3。
🖥️浏览器里的屏幕机器的屏幕显示在浏览器里,支持键盘和鼠标,从 127.0.0.1 访问。
机器之间用 db.lab.internal 互相找到;别的网络连解析都不行。
由 guest 挂载 AWS、R2 或 MinIO,按需读取。
🔑SSH agent 与密钥用 host 的密钥 git push,密钥从不进入 VM。
--expose-socket 和 --mount-socket:双向 Unix socket。
只用标准库。Job、机器、exec 和 streaming。Go:go get runner.codes/pkg/client。
通过 CLI、API 和 SDK 截图、点击、键盘输入和滚动,VM 里无需安装任何东西。
🤖Agent 操作 VMrc mcp:Claude Code 在机器屏幕上截图、点击和输入。
用它能搭出什么。
下面每个场景都在真实的 VM 中运行过,旁边就是用到的命令。
一个不设刹车的 agent,关在盒子里。
Claude Code 在 MicroVM 中以 --dangerously-skip-permissions 运行:它会直接编辑、安装和执行,无需确认,因为出了问题也只留在 VM 里。
- 项目挂载到
/workspace:agent 写的内容会出现在你的目录里。 - 浏览器中的终端,只监听
127.0.0.1,URL 带 token。 - 它打开的任何端口(uvicorn、Vite、数据库)都会变成一个内嵌应用的标签页,与
shell和eventos标签页并列。 - 你的 SSH 密钥通过 host 的 agent 使用:
git push可用,私钥从不进入 VM。 - 只需登录一次:会话保存在机器磁盘上,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)
一台属于你的机器,现在就在浏览器里。
在 claude-in-chrome.runner.codes 每位访客都有一台自己的机器,Claude Code 已打开,不需要确认权限。
- 你的账号:
/login用你自己的账号,只留在你的机器上。 - 任何端口:你启动的服务都会在
https://<端口>-<id>.runner.codes/响应,并成为一个标签页。 - 隔离:互联网开放,服务商的私有网络关闭。
- 临时:空闲 30 分钟关机,24 小时未使用即删除;同时最多 3 台。
https://8000-<id>.runner.codes/ 打开。运行你的用户提交的代码。
练习题、自动评分、文档 sandbox:每次提交都在一台全新的、无网络的 MicroVM 中运行,结束即消失。
- 同一个路由支持 15 种语言。
- 预热好的 pool:每个请求 26–43 ms。
- 通过 SSE 实时输出,超时会杀掉整台 VM。
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}在 Mac 的 GPU 上运行一个隔离的本地 LLM。
VM 内的 llama.cpp 使用 Vulkan,通过 virtio-gpu(Venus → MoltenVK → Metal)调用 host 的 GPU。
- GPU 上 56.7 tokens/s,CPU 上 10.3(Qwen2.5 0.5B)。
- 无法访问你的磁盘:除非你声明了卷,否则不会挂载任何 host 目录。
rc image build --stack llama rc run --stack llama --gpu --code 'vulkaninfo --summary | grep deviceName'
一个标签页里的 Claude Code 桌面。
一个 runner.code 里的 XFCE、Firefox 和 Claude Code,屏幕在浏览器里。屏幕跟随窗口大小,agent 通过 rc mcp 操作 VM:截图、点击和键盘。
- 只监听 127.0.0.1,无需 VNC 或额外客户端。
- 从已就绪桌面的
rc pack5.6 秒启动。
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
# 屏幕地址 http://127.0.0.1:6080代码能碰到什么,由你决定。
机器创建时没有网络,也没有你磁盘上的任何东西。每一处放开都是一行声明。勾选你需要的,看看 runner.code、等价的命令,以及里面的代码能碰到的东西有什么变化。
runner.code
同样的事,用命令行
机器里的代码能碰到什么
运行不是你写的代码。
AI 生成的、用户提交的或从网上下载的代码,在运行时碰不到你的文件、网络和系统。
AI 生成的代码
LLM 写的东西不用先逐行审查就能运行。出了问题,也只在 MicroVM 里出问题。
给 agent 的机器
Claude Code 或 opencode,挂载仓库,网络只通向模型 API,密钥作为 secret,再加 host 的 SSH。每次尝试前先 branch 一下。
Playground 与评测
通过 API 运行用户提交的 15 种语言代码,池子预热好:每个请求 26–43 ms。
比容器更强
容器只是在同一个系统里隔开进程。MicroVM 是另一台电脑:那里出的问题碰不到你的。
什么时候不用:如果代码是你自己的、可信且需要网络,容器就够了,也更简单。
| 容器 | runner.codes | |
|---|---|---|
| 内核 | 与 host 相同 | 独立,每台机器一个 |
| 网络 | 默认开启 | 默认关闭;按 host 逐个放开,每次拒绝都记日志 |
| 凭据 | 进程可读的变量 | placeholder;真实值在 host 上注入,只发往允许的 host |
| 磁盘 | 可写 | job:只读 root;机器:独立磁盘,支持 copy-on-write branch |
| job 结束 | 进程退出 | 整台 MicroVM 消失 |
一个 job、一台机器,或一个 API。
一次性 job
新建 MicroVM,执行,返回 stdout 和 stderr,然后消失。运行一段代码最直接的方式。
- 程序运行时 stdout 和 stderr 实时出现
rc shell在一次性 MicroVM 里打开交互式 shell
# 运行并返回输出 rc run --stack python --code 'print(2**10)' 1024 # 交互式 shell,exit 后 MicroVM 随之消失 rc shell --stack python
持久化机器
可写磁盘在多次 start 之间保留,可基于 stack 或任意 OCI 镜像。用 runner.code 文件声明,与代码一起纳入版本管理。
create,start,exec,shell,cp,logs,stop,deletebranch,pause,resume,sync,update(机器运行中改网络)rc pack create --from-vm打包成可执行文件
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
HTTP API
每种语言一个预先启动的 MicroVM 池,每台只用一次:一个请求 26–43 ms。支持 streaming 输出(SSE),整台机器也能通过 API 管理。
- 没有认证:所以默认只监听
127.0.0.1 - JSON 里的未知字段会被拒绝,而不是忽略
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}亲手测试,一步一步来。
和 task runbook 自动执行并核对的是同一套流程:上一轮 146 项检查,0 失败。复制每一步的命令,与预期结果对照后打勾。进度保存在这个浏览器里。
在一个新终端里逐条执行命令,确认 rc 做到了它承诺的事。每一步都写明你 应该看到什么。
这个 runbook 是可执行的。 task runbook(scripts/runbook.sh)会原样执行这些步骤,并核对每一个预期 结果。只要有一项不同就判定失败。项目规则:每个新功能都要在同一次交付中,在这里新增一节,并在 scripts/runbook.sh 中加入对应的步骤。
| 最近一次执行 | Host | 结果 |
|---|---|---|
| 20/10/2026 | macOS 27, Apple M1 Pro | task runbook:116 项核对 0 失败(到 8u 为止:用户、secret、socket 桥接、ping、staged、拒绝) |
| 19/10/2026 | macOS 27, Apple M1 Pro | task runbook:50 项核对 0 失败(含卷和网络,步骤 8c 和 8d)。失败验证:网关的 DNS 不做过滤时,8d 有 2 项核对失败 |
前置条件:task krun:install、task build 以及镜像(./bin/rc image build --all;第 9 步需要 llama)。
运行时请先停掉 e2e:测试会用 pgrep 确认没有残留的 VM,你自己开着的机器会导致它们 失败。
0准备
cd ~/works/labs/github/runner.codes ./bin/rc doctor
预期:platform、libkrun、gpu 和 guest 均为 ok,并且每个已构建的镜像有一行 ok image <stack>。
1带 streaming 输出的临时 job
./bin/rc run --stack python --code $'import time\nprint("early")\ntime.sleep(1.5)\nprint("late")'预期:early 立即出现(约 0.3 秒,即 boot 时间),late 在 1.5 秒后出现,此时程序仍在运行。最后在 stderr 中输出 [status=success exit=0 ...]。结束时 VM 被销毁。
2创建并启动一台机器
./bin/rc machine create --name teste --stack python ./bin/rc machine start --name teste ./bin/rc machine ls
预期:
created teste (stack=python cpus=4 memory=8192MiB disk=10GiB gpu=false);started teste (pid N) in ~200ms;ls中teste显示为running。
命令退出后机器仍保持运行。
3在三个位置写入内容
./bin/rc machine exec --name teste -- sh -c 'echo persistiu > /etc/minha-config; echo dados > /workspace/arquivo.txt; echo some > /tmp/temporario.txt'
预期:没有任何输出,rc 为 0。
4停止后再次启动
./bin/rc machine stop --name teste ./bin/rc machine start --name teste
预期:stopped teste 和 started teste (pid N) in ~200ms。stop 在关机前会对磁盘执行 sync。
5检查保留下来的内容
./bin/rc machine exec --name teste -- sh -c 'cat /etc/minha-config /workspace/arquivo.txt; cat /tmp/temporario.txt'
预期:
persistiu dados cat: can't open '/tmp/temporario.txt': No such file or directory
/etc(镜像之上的可写层)和 /workspace(机器的磁盘)会持久保留;/tmp 是 tmpfs,重启后为空。
6把文件复制到机器里再复制回来
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"
预期:
uploaded 20000000 bytes ... in ~230ms;- 机器内外的 sha256 相同;
downloaded ... in ~230ms;idênticos。
7机器内的退出码与 streaming
./bin/rc machine exec --name teste -- sh -c 'echo a; sleep 1; echo b; exit 7'; echo "rc=$?"
预期:a,一秒后 b,以及 rc=7。rc 的退出码就是命令的退出码。
7b机器上的交互式终端
./bin/rc machine shell --name teste
在打开的 shell 中输入:
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
退出后,echo $? 显示 4。机器仍在运行:shell 是一个会话,而不是 VM 本身。
预期:真正的终端,支持 Ctrl+C、方向键、历史记录、vi 以及窗口大小调整。镜像中有 bash 时就用它; python 镜像(Alpine)中是 sh。
8机器日志
./bin/rc machine logs --name teste --tail 10
预期:控制台的最后几行(guest 会记录每个操作)。机器停止时同样可用。
8b在 runner.code 中声明的机器
runner.code 是用文件声明的机器:stack 或镜像、资源、env、init 和 workload,都写在一个可版本化的 TOML 中。
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
预期:
- 不带
-f的create显示from runner.code,status 显示"cpus": 1:flag 优先于文件; logs --source workload显示up hello in /app:workdir 已被创建,env 和 workdir 都传到了 workload;- 第一次
exec输出1、hello、/app; - 第二次输出
1和saved:第二次 start 时 init 没有再次运行,而 stop 发出的SIGINT到达了 workload,它在 sync 之前写入了文件。
runner.code 尚未实现的键会被拒绝,绝不会被忽略:
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
预期:key "health" is not supported yet: health checks,且不会创建任何东西。
8c卷:在机器内使用 host 目录
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
预期:
do host:guest 能读取 host 的目录;Read-only file system和ro=1::ro由 libkrun 在设备层强制执行,guest 的 root 也无法解除;- 在 host 上,
cat显示do-guest:guest 的写入立即出现在 host 上。
8d网络:出站、策略与端口
机器创建时没有网络。--net 开启出站;--allow-host 开启并加以限制;-p 把端口发布到 host 的 127.0.0.1 上。
./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
预期:
example.com返回ok=0;- 立即出现
wget: bad address 'iana.org':网关的 DNS 返回 NXDOMAIN,无需等待超时; meta=1:即使网络已开放,云 metadata(169.254.169.254)也会被拒绝;update之后,iana.org无需重启机器即可访问;- host 上的
curl通过发布的端口收到do-guest; - 拒绝日志中有
deny dns iana.org: name not in the allow list。
8e临时机器: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 ls预期:
ola和rc=3:退出码就是命令的退出码;leu:dados:-i会转发 stdin(不带-i时不读取 stdin);- 在目录之外的镜像(首次使用时构建)中输出 libkrunfw 内核的
uname -a; ls中没有任何run-…:临时机器在结束时消失,按 Ctrl+C 也一样。
8f发布的端口能访问只监听 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 -f预期:200。发布的端口经由 guest 进入(vsock 上的 dial 操作),并在那里拨号 127.0.0.1,因此 只监听 localhost 的服务(例如 vite 或 openclaw)也能响应。
8g浏览器中的屏幕:显示、键盘和鼠标
./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 &)'
# 点击页面并输入几个字母;然后:
./bin/rc machine exec --name tela -- grep 'value 1' /tmp/k.log
./bin/rc machine delete --name tela -f预期:
- guest 列出
runner.code keyboard和runner.code tablet; - 页面实时显示
modetest的彩条,分辨率 1024x768; - 在页面上按下的每个键都会出现在 guest 的
evtest中(KEY_R、KEY_C……)。
task runbook 无需人眼即可核对同一路径:1024x768 的画面到达查看器,通过 POST /input 发送的 KEY_A 出现在 evtest 中。
8h自适应屏幕与屏幕控制
与 8g 相同的带屏幕机器,这次通过 CLI、API 和 MCP 控制,分辨率由页面请求:
./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 -f预期:
/tmp/resize.txt中是900 600:页面请求的尺寸到达 guest 的 hook/etc/rc/display-resize(在桌面上它运行xrandr;没有该 hook 时什么也不会发生);screen 1024x768 -> tela.jpg,一张屏幕的 JPEG;evtest中:按下了KEY_LEFTCTRL和KEY_C,ABS_X为32767(右边缘),以及BTN_LEFT;- 查看器页面在 CLI 点击的位置画出橙色箭头(
GET /pointer记录到click); - 屏幕页面的标题栏显示
runner.codes(runner.code里的display_title可以改名); key hyper以错误退出(unknown key "hyper"),不向机器发送任何内容;rc mcp响应initialize并列出 9 个工具:screenshot、click、move、drag、type、key、scroll、actions和exec。
task runbook 执行同样的步骤,并额外核对 API:GET /api/v1/machines/:name/screen 返回 X-Screen-Width: 1024, POST .../screen/actions 对无效动作返回 400,对有效动作返回 204。
8iBranch 与暂停
./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
预期:
status显示paused,在此状态下的exec会被拒绝并给出原因;- 子机器显示
antes:即使母机器处于暂停状态,branch 也会带走它写入的内容(rc会短暂恢复它 以执行sync); - 此后母机器与子机器相互独立。
8j命名网络
./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
预期:
- 两台机器获得该网络中连续的 IP(
db是100.98.0.10,app是100.98.0.11),无需--net即可通过 IP 和名称<máquina>.<rede>.internal互相访问:网络内部的流量不算出站; - 其他网络中的机器无法访问它们,也无法解析它们的名称;
- 删除的
db会从网络的 DNS 中消失:nslookup不再返回它的 IP。
8kS3 卷
一个本地 S3,服务于一个文件夹(MinIO 已于 11/10/2026 从 nixpkgs 中移除):
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
预期:
start挂载 bucket(密钥错误时会失败,输出s3: 403及原因);- guest 读到
ola do bucket,novo.txt出现在服务端的文件夹中,内容为do-guest。
8l机器使用但无法读取的凭据
./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 -f预期:
guest: RC_PLACEHOLDER_TOK_...:guest 只拿到 placeholder;server: Bearer valor-do-runbook:服务端收到了真实值,它来自start的环境变量。
之所以使用自定义 User-Agent,是因为 postman-echo 的 Cloudflare 会拒绝 Python-urllib(403,error code 1010)。
8mPack:把机器装进一个文件
./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
预期:
marca-do-runbook:即使源机器已被删除,产物也带走了机器的磁盘(丢失磁盘的 pack 仍然能启动并以 0 退出;所以要核对一个具体的值);- 单独运行
/tmp/one显示usage: one run ...。
8n用户、磁盘卷与 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"预期:
nobody,以及带--user root时的root(exec 的--user优先于机器的设置);ext2/ext3:磁盘卷是真正的 ext4,位于 overlay 之外(这正是 Docker 的 overlay2 所需要的);do-guest:ping:host 通过机器目录中的docker.sock与 guest 中的服务通信。
8oSecret 与 SSH agent
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)"
预期:
valor-secreto do-arquivo:两个 secret 都在每条命令的环境中;ssh-add -l列出/tmp/rb-id的密钥(私钥留在 host 的 agent 中);grep什么也找不到:该值不会留在机器的记录中。
agent 和密钥都是这一步专用、用完即弃的:不会触碰运行者自己的 agent。
8p双向的 socket 桥接
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())'预期:do-guest:ping(host 能访问 workload 监听的 socket)和 do-host:pong(guest 能访问 host 的 socket,位于镜像中原本不存在的目录里)。
8qPing 与端口范围
./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"预期:
200:端口范围按编号逐个发布端口(最多 64 个),中间那个有响应;- 网络开放时,对
1.1.1.1的 ping 会返回,且仅当 host 收到了回复时才返回(不会伪造回复); - 在 allowlist 之外 ping 不会返回,拒绝记录
deny icmp 1.1.1.1写入日志。
8rStaged 卷与 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
预期:
ext2/ext3:副本位于机器的磁盘上,无需经过 virtio-fs 往返;- sync 之前 host 上仍是
original; - 之后:
mudou,没有old.txt(sync 是镜像式的:副本中消失的内容也会从 host 上消失),run.sh为-rwxr-xr-x。
优雅停止和 machine run 结束时也会同步;--timeout 0 不会。branch 出来的子机器不会向 host 回写任何内容。
8s说明原因的拒绝
./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
预期:
info cuda: not supported (no CUDA remoting; --gpu is Vulkan)以及本 host 关于 nested 的那一行;--cuda is not supported: rc has no CUDA remoting ...,且不创建机器;- 在不支持嵌套的 host(M1/M2)上,
failed to start: Error: nested virtualization is not supported on this host ...在 1 秒内出现(以前是 30 秒后报did not become ready);在支持嵌套的 host 上,VM 内有/dev/kvm。
8t机器目录与缓存清理
ls "$(./bin/rc machine data-dir --name usr)" ./bin/rc image prune ./bin/rc pack prune
预期:机器目录中有 machine.json;两个 prune 都以 0 退出(不带 --all 时,image prune 只删除 不再被任何名称引用的 ext4,即某个镜像之前的构建,而 pack prune 保留最近使用的 5 个)。
8u指南
task guides DIRS=gpu-cuda # uma pasta; sem DIRS, todas (cada uma liga as suas VMs)
预期:每个文件夹都是 falhas: 0。每个文件夹验证的内容见 examples/guides/README.md。
8vWeb 终端
./bin/rc machine web-terminal --name demo # 在浏览器中打开机器的 shell ./bin/rc machine web-terminal --name demo --no-open -- sh -c 'echo wt-ok:$((6*7))'
预期:
web terminal: http://127.0.0.1:端口/?t=<32 位十六进制>:只在回环地址,带 token;浏览器会自动打开 (--no-open只打印 URL);- 浏览器中是真正的终端(xterm.js):Ctrl+C、方向键、全屏编辑器和窗口大小都会传到 guest,刷新后会重新显示最近的屏幕;
- 带命令时,屏幕显示
wt-ok:42,页面提示程序已退出,rc以该程序的退出码退出; - 没有 token 的
/stream返回 403,非回环的Host(DNS rebinding)也返回 403。 - 标签页
claude(命令)、shell(第一次点击时打开的独立终端)、eventos,最后是每个端口一个标签页:程序在 VM 中启动的服务器(任意端口)会以 iframe 显示应用,并在 host 的127.0.0.1上响应。
这样在浏览器中打开 Claude Code: rc machine web-terminal --name agent -- claude --dangerously-skip-permissions。
8w公共 playground (claude-in-chrome.runner.codes)
每位访客都有一台自己的 Fly Machine(Fly 不提供 /dev/kvm,所以 microVM 就是 Fly 的),Web 终端里是 Claude Code,端口在 https://<端口>-<id>.runner.codes/。最多 3 台;空闲 30 分钟关机,24 小时未使用则删除。
scripts/playground-cloudflare.sh # claude-in-chrome 隧道、ingress 和 CNAME(幂等) scripts/playground-fly.sh setup # runner-codes-playground 和 runner-codes-visitors 应用、独立网络、secrets scripts/playground-image.sh all # 在 Fly 构建机上用 Nix 构建 visitor 和 front 镜像 scripts/playground-fly.sh deploy # 发布 front scripts/playground-token-check.sh # front 的令牌只能访问访客应用 curl -s https://claude-in-chrome.runner.codes/ | grep -c FingerprintJS.load
预期:
- 页面打开,计算浏览器的 visitorId,并带你进入带 Claude Code 的终端
/v/<id>/?t=<令牌>; - 无效访客调用
/api/session返回 400;3 台机器都在使用时返回 503 "playground is full"; https://3000-0000000000.runner.codes/(不存在的机器)返回 502,docs.runner.codes仍为 200;scripts/playground-token-check.sh:访客应用 200,网站和 front 本身 4xx;- 在访客机器内,连接 Fly 私有网络(
_api.internal)失败,DNS 和互联网正常(scripts/fly-firewall-probe.sh在一台一次性机器上测同一套防火墙)。
8xKubernetes 运行时(k3s 和 microk8s)
每个设置了 runtimeClassName: rc 的 pod 都会成为一台拥有独立内核的 libkrun microVM:containerd 调用 containerd-shim-rc-v2,pause 容器启动 VM,应用容器在其中运行。VM 使用 CNI 分配给 pod 的 IP(在 pod 的 netns 中用 tc mirred 连接一个 TAP)。需要带 /dev/kvm 的 Linux 节点;测试节点是 DigitalOcean 的 droplet (task k8s:kvm-probe 会测量)。
K8S_NODE=k3s task k8s:node -- create && K8S_NODE=k3s task k8s:node -- provision # microk8s 同样用 K8S_NODE=microk8s # 在节点上:curl -sfL https://get.k3s.io | sh - 或 snap install microk8s --classic task k8s:e2e -- k3s # 同步代码、编译、运行 deploy/k3s/install.sh 和 deploy/k8s/e2e.sh k3s task k8s:e2e -- microk8s # 同上,使用 deploy/microk8s/install.sh task k8s:defects # 在运行时代码中故意植入 20 个缺陷,确认测试都能发现
预期(deploy/k8s/e2e.sh 的 19 项检查,两者都是 falhas: 0):
rc-hello进入 Ready,日志显示RC_K8S_OK、VM 的内核(6.12.91,而不是节点的)以及eth0上的 pod IP;kubectl exec无 TTY、有 TTY 和带 stdin 都可用;exec 的退出码(7)和容器的退出码(3)都正确返回;- 集群 DNS、rc pod ↔ runc pod 双向互通,以及通往 rc pod 的 ClusterIP Service;
- 同一 pod 中的两个容器:同一台 VM(相同的
boot_id)和同一个 emptyDir; - kubelet 的统计摘要正常响应(VM 的用量计入第一个容器);
- 删除后,
/var/lib/rc-shim/pods中没有残留目录,也没有残留的rc vmm进程。
已声明的限制:port-forward、hostNetwork、hostIPC 和挂载传播无法穿过 VM;卷在创建容器时被复制到 pod 的共享目录 (之后 kubelet 的更改,例如 serviceaccount token,不会同步进来);CPU/内存限制作用于整台 VM(pod 注解 runner.codes/cpus 和 runner.codes/memory-mib);pod 的 vmm 在没有 Landlock/seccomp 的情况下运行。
9GPU
仅当 llama 镜像已构建时执行(rc doctor 显示 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'
预期:
deviceName = Virtio-GPU Venus (Apple M1 Pro),以及一行llvmpipe(软件实现的 Vulkan);llama-bench的表格中有Vulkan0,pp128在 900–1.365 t/s 范围内,tg32约 57 t/s。
详情见 GPU。
10HTTP API
在一个终端中:
./bin/rc serve --pool-size 0
在另一个终端中:
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; echo预期:
{"exitCode":0,"stdout":"42\n","stderr":"",...};- 列表中有
"name":"teste"和"state":"running"。
替代请求体和按行 stream,在同一个服务上:
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/sync预期:
400:未知字段(allowedHost,拼写错误)会被拒绝,而不是被忽略后让出站保持开放;"allow_hosts":["api.example.com"]、"enabled":true和"memory_mib":512:替代名称同样有效;nestedVirt返回400(只有rc serve --allow-nested-virt时才放行);event: stdout/data: a,然后data: b:每个事件一行原始文本,最后是event: exit;sync返回204(机器没有 staged 卷:无事可做)。
Ctrl+C 结束 serve。机器仍在运行,因为它是分离的。
10bGo 与 Python SDK
serve 仍在运行时:
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
预期(两者):
job: 42(临时 job);machine: sdk-... running和exec: ola do sdk go(或python):机器已创建,文件已写入并执行;error: NOT_FOUND:API 的拒绝以带类型的错误返回,带有稳定的错误码;./bin/rc machine ls中没有残留的sdk-*机器。
10c在外部项目中使用 Go SDK(go get)
Go 模块名为 runner.codes,https://runner.codes 会以指向 GitHub 的 go-import meta 响应 go get。在仓库之外的目录中, serve 仍在运行时:
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
预期:
curl返回<meta name="go-import" content="runner.codes git https://github.com/andrebassi/runner-codes">;go get下载runner.codes v1.1.0(或更新版本);go run的输出与 10b 相同:job: 42、exec: ola do sdk go和error: NOT_FOUND。
Python 同理,在干净的 venv 中从 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 <仓库>/examples/sdk-python/main.py http://127.0.0.1:8080
预期:pip 安装 runner-code 1.1.1,输出与 10b 相同(job: 42、exec: ola do sdk python、error: NOT_FOUND)。 -I 确保包来自 venv,而不是仓库。
10dSDK 示例
SDK 页面中的七个示例,分别用 Go 和 Python,serve 仍在运行时:
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
预期(两者;时间会变化):
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
第二个程序的 machine 行中是 sdk-python 而不是 sdk-go。./bin/rc machine ls 中没有残留的 recipe-* 机器。
10e通过 Go SDK 运行带 Claude Code 的桌面
serve 仍在运行时,由 Go SDK 创建并核对 examples/desktop-claude/runner.code 的机器(XFCE、Firefox、Claude Code):
go run ./examples/desktop-claude -server http://127.0.0.1:8080 -name desk -display-port 6081 open http://127.0.0.1:6081
预期输出:
claude 2.1.x (Claude Code)和navegador padrão /usr/lib/firefox-esr/firefox-esr;atalhos Arquivos.desktop Claude.desktop Navegador.desktop Terminal.desktop;tela responde ... current 1200 x 700:经过 200 次鼠标移动后,屏幕依然正常,且保持请求的尺寸;terminal maximizado _NET_WM_STATE_MAXIMIZED_HORZ;sessão de volta em 1200x700:程序结束 XFCE 会话(相当于注销),会话会自行恢复。
在浏览器中:黑色背景、四个快捷方式,屏幕与窗口同尺寸。
10f向机器中的 Claude Code 派发任务
需要机器上的 Claude 已登录:token 通过 runner.code 的凭据进入(guest 只看到 placeholder),来自执行 start 者的 环境。在环境中带着 token 启动 serve:
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
预期:✓ /root/agent/headless.txt、✓ /root/agent/gui.txt 和 ✓ /root/agent/keys.txt,每个文件内容为 feito pelo modo <modo>。这是派发任务的三种方式:通过 exec 运行 claude -p、用 xdotool 在窗口中输入,以及通过 屏幕 API 输入。后两种方式的屏幕截图保存在 agent-gui.jpg 和 agent-keys.jpg。
task runbook 只有在设置 RB_CLAUDE=1 且环境中有 token 时才运行此步骤,因为它使用 Claude 的订阅。
11清理
./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
预期:stopped teste 和 deleted teste。./bin/rc machine ls 恢复为空。
extra结果不一致时
| 症状 | 排查位置 |
|---|---|
image for X not built | ./bin/rc image build --stack X |
read stream: the guest in this image does not support streaming | streaming 之前的镜像:重新构建 |
INVALID_STATE: machine X is not running | ./bin/rc machine start --name X |
CONFLICT: machine X already exists | 换个名称,或先 delete |
start 失败并显示控制台末尾 | ./bin/rc machine logs --name X |
GPU:ERROR_OUT_OF_HOST_MEMORY | GPU › 为什么用 Fedora |
| 停不下来的机器 | ./bin/rc machine stop --name X --timeout 0 立即杀掉 |
来源:docs/runbook.md · 可执行:scripts/runbook.sh
这里没有一句空话。
每个功能都有单元测试、一次真实 MicroVM 上的验证,以及故意植入的缺陷,用来确认测试会失败。
我在新功能里故意植入了 28 个缺陷(网络、ICMP、staged、socket、nested、SSE…),测试全部抓住了这 28 个。2026-10-20 那一轮,macOS 27,M1 Pro。
快到不必再用容器。
启动时间于 2026-10-19 测得(30 次启动),branch 于 2026-10-20 测得,均在 Apple Silicon Mac 上。完整报告见文档。
目录里 15 种,外加一个 LLM。
每种都有自己的镜像、CPU 与内存上限和预热步骤,写在一个 YAML 里。机器可以使用任意 OCI 镜像 (redis:7-alpine, postgres:16-alpine…).
- Branch、暂停与磁盘 checkpoint 就绪
- 按 host、CIDR 和命名网络控制,含 ping 就绪
- 读不到的凭据、secret 与 SSH agent 就绪
- 可移植 pack、staged 卷与 S3 bucket 就绪
--nested(VM 里的 KVM) 只测了拒绝路径
--nested 需要 M3 或更新的 Mac,或支持嵌套 KVM 的 Linux。在这台 M1 上它会立刻拒绝并说明原因。
Mac 和 Linux,无需 Docker。
runner.codes 使用系统自带的 hypervisor。不需要 Docker,也不需要云主机。
Apple Silicon,通过 Hypervisor.framework。本页的数字都在这里测得。
x86_64 和 aarch64,通过 KVM(你的用户需要能访问 /dev/kvm)。每台 MicroVM 的进程都在 Landlock 和 seccomp 下运行。真实 KVM 的 Linux 上的启动时间尚未测量。
libkrun 没有 Hyper-V 后端。办法是开启嵌套虚拟化的 WSL2,在里面运行 Linux 版 rc。
开箱即跑,都在 VM 里测过。
每个示例都在真实 VM 上启动,每篇指南都有一个 run.sh 证明它说到做到。
三条命令,全部走 Nix。
不用 Homebrew,也不用 Docker。macOS 上第一次安装会编译 libkrun(约 10 分钟);Linux 上直接用 nixpkgs 的预编译包,你的用户需要能访问 /dev/kvm。
./bin/rc doctor 会检查 host 是否就绪。
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")'