- 虚拟化
- AI Agent
- 人工智能
- CLI
【免费下载链接】smolvm
An embeddable, portable, branchable virtual machine to safely run Agents locally.
本指南围绕 smolvm 的本地 HTTP API(smolvm serve)展开,讲解如何在不 shell 出 CLI 的情况下,通过 HTTP 完成虚拟机的创建、启动、执行命令、流式输出、文件上传下载与销毁的完整生命周期,并覆盖 pause/resume 暂停恢复、安全传输选择与平台差异。读完本文,你将掌握一套可复现的端到端验证脚本、易踩坑的 API 字段清单,以及"HTTP 200 不代表成功"的核心调试思维,可直接用于构建 Agent 工具、MCP 后端或测试 Harness。
该文档与验证脚本已在smolvm v1.23.0(macOS arm64,2026-10-04)与v1.18.2(Linux aarch64,2026-09-24)上验证通过:机器走完 HTTP 全生命周期后,GET /api/v1/machines/<name>最终返回NOT_FOUND。
贯穿全文的两条铁律:200 不是结果
本指南所有操作都遵循同一形态的两条规则,它们的本质是同一个:HTTP 200 不构成结果,必须从响应体中取断言值。
- 客机内失败的命令仍然返回 HTTP 200。必须断言响应体中的
exitCode,而不是 HTTP 状态码。相关实现见 exec.rs 中的ExecResponse:exec返回 stdout 的文本与 base64 两种形式(stdout与stdoutB64),exec/stream则按行发出event: stdout,最后以event: exit收尾。 - 文件上传必须等到 workload 容器运行之后。容器启动前的上传会返回 200 并给出已解析路径与字节数,但文件随后不可读。
这两条规则贯穿下文所有环节,scripts/lifecycle-check.sh的设计也围绕它们展开。
五步标准流程
1. 前置检查:Preflight
scripts/preflight.sh该脚本是只读的:不启动 VM、不启动服务器、不写 smolvm 状态。输出为每行key=value,最后一行恒为result=ready或result=blocked。关键输出项包括:
auth=none_unless_mtls——这不是对你环境的警告:按本流程方式启动的服务器没有任何 TLS、token 或鉴权,你选择的传输方式就是访问控制本身。互信 TLS(mTLS)存在,但从环境变量配置而非命令行参数开启,详见 references/traps.md。version_status=match|newer|older——脚本在 v1.23.0 上验证,若你的二进制版本不同,会给出提示,提醒"每个 release 的命令行与报错文案都会变动"。- Linux 下检查
/dev/kvm是否存在且可读写;macOS 下检查kern.hv_support与 socket 路径长度(sockaddr_un在 macOS 上只有 104 字节,HOME过深会导致每个 VM 启动以krun_start_enter -22失败)。
2. 启动服务器:Unix socket 优先
scripts/serve-start.sh scripts/serve-start.sh --listen 127.0.0.1:8899脚本的行为与直接跑smolvm serve start有几个关键差异:
- 它等待
/health返回"status":"ok"而不是等待进程存在(源码注释指出:服务器在能应答之前就会写出监听行,而一个死掉的服务器会留下过期的 pid)。超时 60 秒,实测两台主机均 1 秒应答。 - 它记录监听地址与 pid 到
SMOLVM_SKILL_STATE_DIR(默认$XDG_STATE_HOME/smolvm-skills,macOS 无 XDG 时回落到$HOME/.local/state)供后续脚本复用,并在退出时清理。 - 若已记录的服务器仍在运行,再次执行会打印
result=already_serving并带上已记录的地址,无论--listen传什么都不会重复启动(因为第二个服务器会因 rollout 端口冲突而退出,见下文 Traps)。 - 它会转述启动日志中的
Reclaimed N dangling VM data dir(es)行——serve start启动时会回收崩溃或强杀运行留下的残留 VM 目录,看到这行不要误报为泄漏。
Unix socket 是这里的默认,也是 smolvm 的默认。原因很直白:没有 mTLS 的普通服务器没有任何鉴权,socket 的文件权限是唯一的边界;loopback TCP 的边界则等同于"主机上每个进程都在边界之内"。
3. 导出 OpenAPI spec:写请求体之前做这件事
smolvm serve openapi -o ./openapi.json这一步省时间最多。注意两点:
- 字段名是 schema 的,不是 CLI flag 的:
network不是net,memoryMb不是memory,cmd是你原本想放在--之后的 workload。字段清单见 references/api-fields.md:allowedCidrs、allowedHosts、autoGraph、blobPeers、cmd、cpus、cuda、dockerSocket、entrypoint、env、from、gpu、image、memoryMb、mounts、name、network、networkBackend、overlayGb、ports、registryIdentityToken、registryRef、restart、secrets、storageGb、workdir;v1.14.3 增加blockIo(块 I/O 引擎,缺省不设置即保持本文描述的行为),v1.18.x 增加credentials(凭证替换绑定)与guestSubnet(--guest-subnet的 API 形态),后续版本还增加diskDurability(full或deferred,缺省为full)。请针对自己的二进制导出 spec,不要依赖这份快照清单。 - spec 里的
info.version不是二进制的版本:它写死为0.5.2(v1.14.2 到 v1.22.2 实测均是如此),而同一台服务器的/health报出真实版本。版本请一律取自/health。
从 v1.22.2 起,未知字段会被拒绝并返回422且点名该字段(见 api-fields.md 中的实测响应体);在此之前的版本则照单全收、静默忽略。
4. 跑完整生命周期
scripts/lifecycle-check.sh脚本依次执行:创建 → 启动 → 等待 workload 容器以某个值应答 → exec → 故意 exec 一个失败命令并断言其非零exitCode随 200 返回 → 流式输出 → 文件往返 → 停止 → 删除 → 断言机器已消失。完整输出如下:
created_state=ok (created) started_state=ok (running) workload_ready_after_s=0 workload_ready=ok (yes) exec_exit_code=ok (0) exec_stdout=ok failing_exec_exit_code=ok (3) stream_lines=ok (3) stream_exit_event=ok file_roundtrip=ok (PAYLOAD123) machine_gone=ok (NOT_FOUND) result=lifecycle_ok关键实现细节(与源码对应):
- 等待 workload 容器:脚本用
exec探测echo WORKLOAD_READY的输出,而不是检查某个状态字段。120 秒超时是两台主机中最慢的 workload 容器启动(脚本构建期间实测)的六倍余量。 cmd比看起来重要:不传cmd时,镜像自己的 ENTRYPOINT/CMD 会成为机器的常驻 workload。对解释器镜像(如python:3.12-alpine)来说,该命令读到 EOF 会立刻退出 → 容器被重启 →exec可能落在空档,返回exitCode: 1且stdout 和 stderr 都为空,比 CLI 的等效失败更安静。脚本实测遇到过一次,因此现在总是传长命cmd:["sh","-c","while true; do sleep 3600; done"]。- 断言
exitCode而非 HTTP 状态:脚本专门跑sh -c 'exit 3'并断言exitCode == 3,让"能抓住 200 背后失败"的断言本身也被测试过,而不是假设其有效。 - 文件往返:容器就绪后,
PUT上传PAYLOAD123到%2Ftmp%2Fabs.txt,再GET取回比对。路径是绝对且 URL 编码的。
5. 清理:先删机器,再停服务器
scripts/cleanup.sh --purge顺序至关重要:停掉服务器并不会停掉机器,机器在服务器退出后继续运行(服务器关闭时会打印Shutting down server (VMs continue running)...,这是唯一的提示,且如果 stderr 被重定向很容易错过)。脚本先删除记录过的机器(--cascade连带删除分支子机器),再停服务器,最后移除 socket。
cleanup.sh会先打印waiting=up to 20s并轮询机器列表:临时机器(ephemeral)的条目在其 run 返回后才注销,最多等 20 秒。脚本只删除带smolskill-前缀且被--record记录过的机器,其他会话手工创建的机器永远不会被误删。
核心调用速查
B=http://127.0.0.1:8899 # 或: curl --unix-socket "$SOCK" http://localhost/... curl $B/health curl -X POST $B/api/v1/machines -H 'content-type: application/json' \ -d '{"name":"m","image":"python:3.12-alpine","network":true,"memoryMb":2048, "cmd":["sh","-c","while true; do sleep 3600; done"]}' curl -X POST $B/api/v1/machines/m/start -H 'content-type: application/json' -d '{}' curl -X POST $B/api/v1/machines/m/exec -H 'content-type: application/json' \ -d '{"command":["sh","-c","echo hi"]}' curl -N -X POST $B/api/v1/machines/m/exec/stream -H 'content-type: application/json' \ -d '{"command":["sh","-c","for i in 1 2 3; do echo line$i; sleep 1; done"]}' curl -X PUT "$B/api/v1/machines/m/files/%2Ftmp%2Fabs.txt" --data-binary 'PAYLOAD' curl "$B/api/v1/machines/m/files/%2Ftmp%2Fabs.txt" curl -X POST $B/api/v1/machines/m/stop -H 'content-type: application/json' -d '{}' curl -X DELETE $B/api/v1/machines/m端点全表见 AGENTS.md 的 HTTP API 一节(exec/interactive还会升级为携带 stdin/stdout 双向通道的 WebSocket):
POST /api/v1/machines create GET /api/v1/machines list GET /api/v1/machines/:name get DELETE /api/v1/machines/:name delete POST /api/v1/machines/:name/start start POST /api/v1/machines/:name/stop stop POST /api/v1/machines/:name/exec exec POST /api/v1/machines/:name/exec/stream exec, SSE PUT /api/v1/machines/:name/files/*path upload GET /api/v1/machines/:name/files/*path download GET /api/v1/machines/:name/logs logs, SSE POST /api/v1/machines/:name/images/pull pull POST /api/v1/machines/:name/pause pause POST /api/v1/machines/:name/resume resume补充说明:
- files 路由的
GET对目录返回列表:{"entries":[{"kind":..., "name":..., "size":...}]},与 files.rs 中FilePayload::Directory的实现一致,调用方探索目录树时无需预先知道路径类型。 exec返回 stdout 的文本与 base64 两种形态,exec/stream每行一个event: stdout,末尾一个event: exit。- 错误体携带原因:404 返回
{"error":"machine 'nope-does-not-exist' not found","code":"NOT_FOUND"}。
高频陷阱(Traps)
完整细节见 references/traps.md 与 references/api-fields.md。以下是速查版:
- 上传必须在容器起来之后,绝不能提前。Linux aarch64 上 v1.14.6 与 v1.18.2 实测:PUT 返回
200 {"path":"/tmp/r1.txt","size":6},文件却从未可读;/tmp正是容器会挂载覆盖的路径。v1.16.1 与 v1.22.2 未复现(各主机 0/3),macOS 从未复现。时序相关让它更糟而非更好——一次碰巧成功的运行证明不了下一次。正确做法:先等一次成功的exec再上传(lifecycle-check.sh正是如此),或上传到 overlay 上的路径(如/root)而非容器挂载覆盖的路径。 - 失败的客机命令是 HTTP 200。从响应体断言
exitCode。 - 未知字段 v1.22.2 起拒绝(422 点名),此前静默丢弃。v1.22.2 之前写
memory代替memoryMb不会有任何诊断,机器直接采用默认值。 - 同主机第二个
serve start直接失败:bind guest rollout ingress: 127.0.0.1:10081: Address already in use——无论--listen传什么,每个服务器都绑定 10081 端口用于 branch-pool rollout 路由。第二个服务器需设SMOLVM_GUEST_ROLLOUT_HOST_PORT换端口。 - 停服务器不会停机器。先删机器。
- spec 的
info.version不是二进制的版本(写死0.5.2),版本取自/health。 - 默认监听路径随平台变化,
--help只显示其中之一:help 打印[default: unix:///tmp/smolvm.sock],其示例行又写unix:///$XDG_RUNTIME_DIR/smolvm.sock。实测 v1.16.1:macOS arm64 出现在/tmp/smolvm.sock,Lima aarch64 出现在/run/user/501/smolvm.sock。读取服务器报告的实际路径,而不是猜。
字段名代价矩阵
| 你写的是 | schema 要的是 | 写错的代价 |
|---|---|---|
net | network | 机器没有网络;两主机上实测在 create 时被 400 拦下并给出良好报错(image 'alpine' must be pulled from a registry, but this machine has no network...) |
memory | memoryMb | 无任何诊断,静默取默认值(v1.18.2 实测返回"memoryMb":8192) |
--之后的 workload | cmd(和entrypoint) | 镜像自带 CMD 成为常驻 workload,解释器镜像立刻退出 |
注意:v1.23.0 起(对应 PR #1536)不再返回上述 400,"image":"alpine","network":false会合法创建并成功启动、可 exec——网络关闭被写错时,只有在 workload 访问不到外部时才会暴露;net这个名字本身在 v1.23.0 上仍是 422。另一个对比:Smolfile 会拒绝未知 key(README 明确),API 不会。
文件上传竞态的底层原理
从源码看,双向的文件读写每次请求都选择命名空间:crates/smolvm-agent/src/main.rs中handle_file_write与handle_streaming_file_read都通过nsfile::GuestNs::for_workload()判定——有 workload 容器运行时写入/读取发生在容器内,否则在 agent 的命名空间。容器启动前的写入落在 agent 命名空间,容器一旦启动,读路径解析进容器内;对容器挂载覆盖的路径(如/tmp,客机内是 tmpfs),之前播种的文件不再位于被读的视图里。源码注释声称"容器启动前播种正是文件在容器启动后可见的方式"——这只对 overlay 路径成立,对容器挂载覆盖的路径不成立。实测复现:PUT 立即 GET 报failed to canonicalize target /tmp/r1.txt: No such file or directory,30 秒后再 GET 报failed to read /tmp/r1.txt in the workload container: ...——两种报错文本正是两个命名空间:容器前根本无法 canonicalize 路径,容器后读发生在容器内而落空。
通过 API 暂停与恢复机器
v1.18.0 新增POST /api/v1/machines/{name}/pause与/resume。Pause 会保存 RAM、磁盘与正在运行的执行并停止机器;resume 在同名下恢复该执行,而不是启动一个全新客机。实现见 machines.rs 的pause_machine/resume_machine(resume_machine_detached_with_operation等运行时路径)。
macOS 上 v1.20.0 之前,机器必须以可分支(branchable)方式启动,API 形态是 start 调用上的查询参数,下面调用总是带上它:
curl -X POST "$B/api/v1/machines/m/start?branchable=true" -H 'content-type: application/json' -d '{}' curl -X POST $B/api/v1/machines/m/pause -H 'content-type: application/json' -d '{}' # state: paused curl -X POST $B/api/v1/machines/m/resume -H 'content-type: application/json' -d '{}' # state: running断言 workload 持有的内存值,不要断言 state 字段。实测(v1.18.2 双主机、v1.22.2 再测 macOS):workload 每秒写一个递增计数器,pause 前读到 9,暂停 10 秒后 resume 3 秒时读到 13(macOS)与 12(Linux)——进程从暂停处继续、没有从 1 重启、暂停期间没有运行。CLI 下,已暂停的机器拒绝stop和start;docs/teardown记录相关报错,docs/branch-and-checkpoint把 pause 与 checkpoint 一并覆盖。
安全默认值与为什么是默认值
- Unix socket 是默认,因为它是普通服务器唯一的访问控制。没有 mTLS 环境变量的服务器没有任何 TLS、证书、token 或鉴权,而它暴露的路由能创建机器、执行任意命令、读写文件。socket 的文件权限是真实边界;loopback 端口只是"主机上每个进程都在边界之内"意义上的边界。
- loopback TCP 用于你需要 URL 的场景:容器网络命名空间内,或客户端不支持 Unix socket 时。把端口视同主机上的 shell,只绑定
127.0.0.1。 scripts/serve-start.sh把 socket 放在本流程自己的状态目录下,而非全局可遍历的临时目录,并在清理时移除。- 机器先于服务器删除,因为停服务器不停机器:
serve start --help说明机器独立于服务器持久存在,关闭时打印Shutting down server (VMs continue running)...。 - mTLS 从环境而非 flag 配置:设置
SMOLVM_SERVE_TLS_CERT、SMOLVM_SERVE_TLS_KEY、SMOLVM_SERVE_TLS_CLIENT_CA后(v1.22.2 macOS 实测):无证书的客户端在握手阶段被拒,CA 签发的证书可从/health得到{"status":"ok","version":"1.22.2",...},--mtls-client-cn设为其他名称时同一证书被拒;不带--mtls-client-cn时服务器会警告 CA 签发的任何证书都有完全访问权。mTLS 服务器还会在旁边开一个纯明文 loopback 监听器(smolvm local API (loopback, plain) on http://127.0.0.1:<port + 1>,SMOLVM_SERVE_LOCAL_ADDR可移动,仅限 loopback 地址),它无鉴权且只应答/health、/readyz、/capacity、/metrics,机器/exec/文件路由在 TLS 端口上。v1.22.1 起还有--mtls-client-cn(限制 API 访问到该 subject CN 的客户端证书)与--mtls-allow-peer-blobs(仅让客户端 CA 的其他证书到达 peer blob 路由),对应 serve.rs 的mtls_client_cn/mtls_allow_peer_blobs参数。
平台矩阵
- macOS arm64:v1.23.0 走 loopback TCP,v1.22.2 同时验证 Unix socket。
result=lifecycle_ok,全部十一项检查通过。 - Linux aarch64:v1.18.2 走 Unix socket(2026-09-24),
result=lifecycle_ok十一项全过。v1.22.2 唯一一次失败仅发生在machines_empty(当时的检查要求机器列表为空,而一次被中断的运行留下了两个image-seed-*helper);v1.23.0 当前脚本走 Unix socket 一次通过。v1.23.0 起默认含--seccomp enforce(arm64,对应 PR #1533),serve start的--seccomp参数在 serve.rs 中可选enforce|audit|off。 - Linux x86_64:v1.14.2 在 NVIDIA A10 云主机上双传输方式验证过,此后未再复跑。
- Windows x86_64:见 references/windows.md,2026-10-03 在 Windows 11 Home(build 10.0.26200 UBR 9457)上对 v1.22.2 重跑,全生命周期走 loopback TCP 通过;
unix://监听在那里被拒绝。错误体与 Unix 一致,但 PowerShell 5.1 的Invoke-WebRequest会丢弃它们,需要用curl.exe读取。路由位于/api/v1/下;v1.21.0 起无 body 的startPOST 也会被接受。
验证记录:这些结论从哪来
本流程的每个断言都来自可复现的运行,而不是假设:
- "写一个端到端通过 HTTP 驱动 smolvm 机器并证明它工作"→
scripts/lifecycle-check.sh。十一项检查在 macOS arm64 v1.22.2(loopback TCP 与 Unix socket 双通道)与 v1.23.0(macOS loopback TCP、Linux Unix socket)全部通过,输出即上文 step 4,含failing_exec_exit_code=ok (3)——这个专门抓住"失败藏在 200 后面"的断言。最后的machine_gone检查 2026-10-03 走 loopback TCP、2026-10-04 走 Unix socket。 - "启动机器后立刻上传文件,现在 API 说它不存在"→ Linux aarch64 上 v1.14.6 与 v1.18.2 按此操作复现,输出记录在 references/traps.md;v1.22.2 未复现,各主机 0/3。
- "我的 create 返回 200 但机器设置不对"→ v1.22.2 上错误命名字段在任何东西创建前就被拒绝;此前版本则接受并丢弃(见 api-fields.md 的实测响应)。真实响应:
POST /api/v1/machines {"name":"...","image":"alpine","network":true,"memory":1024} -> 422 Failed to deserialize the JSON body into the target type: memory: unknown field `memory`, expected one of `name`, `cpus`, `memoryMb`, `mounts`, `ports`, `network`, ... at line 1 column 63- 未覆盖的部分:Linux x86_64、Linux 上的 loopback TCP、mTLS 服务器上的完整生命周期客户端(握手在 v1.22.2 验证过:无证书客户端被拒、有证书客户端
/health应答,但未通过它创建机器)、以及 spec 中属于 branch-pool 特性的 pool 与 rollout-executor 路由。spec 中没有 checkpoint 路由,但服务器实际有一个:POST /api/v1/machines/{id}/checkpoint捕获运行中的机器、同路径PUT恢复,from字段也可恢复 checkpoint——本流程未实际演练。
相关文档导航
- docs/local-api/README.md:本主题的概览与端点总表。
- docs/local-api/references/api-fields.md:create 请求体字段全表、错误字段代价、422 实测响应。
- docs/local-api/references/traps.md:上传竞态原理、mTLS 实测、第二服务器端口冲突等完整陷阱。
- docs/local-api/references/windows.md:Windows 平台细节。
- docs/local-api/scripts/lifecycle-check.sh:十一项端到端断言脚本。
- docs/local-api/scripts/serve-start.sh:服务器启动与健康等待脚本。
- AGENTS.md:完整 HTTP API 端点表与安全说明。
- docs/install:本文假设的引导安装;docs/teardown:清理脚本配套;docs/dev-env:同一生命周期的 CLI 形态与
cmd字段所要解决的 workload 容器行为。
- 虚拟化
- AI Agent
- 人工智能
- CLI
【免费下载链接】smolvm
An embeddable, portable, branchable virtual machine to safely run Agents locally.
相关推荐
smolvm 本地 HTTP API 实战指南:以 HTTP 驱动虚拟机全生命周期
smolvm 本地 HTTP API 实战指南:以 HTTP 驱动虚拟机全生命周期 导读 本指南围绕 smolvm 的本地 HTTP API( smolvm s
虚拟化AI Agent人工智能CLIsmolvm 无驱动微虚拟机运行 CUDA:--cuda API 远端化(API Remoting)实战指南
smolvm 无驱动微虚拟机运行 CUDA: cuda API 远端化(API Remoting)实战指南 导读 本文基于 docs/gpu cuda/SKIL
虚拟化AI Agent人工智能CLIsmolvm CUDA 远程调用(--cuda):在无驱动微虚拟机中安全运行 NVIDIA 计算负载
smolvm CUDA 远程调用( cuda):在无驱动微虚拟机中安全运行 NVIDIA 计算负载 smolvm 通过 cuda 把客户机(guest)的 CU
虚拟化AI Agent人工智能CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考