news 2026/10/9 5:10:05

smolvm 本地 HTTP API 实战指南:用 smolvm serve 编程式驱动虚拟机全生命周期

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
smolvm 本地 HTTP API 实战指南:用 smolvm serve 编程式驱动虚拟机全生命周期
  • 虚拟化
  • AI Agent
  • 人工智能
  • CLI

【免费下载链接】smolvm

An embeddable, portable, branchable virtual machine to safely run Agents locally.

项目地址:https://gitcode.com/gh_mirrors/sm/smolvm
点击查看免费下载

本指南围绕 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 不构成结果,必须从响应体中取断言值。

  1. 客机内失败的命令仍然返回 HTTP 200。必须断言响应体中的exitCode,而不是 HTTP 状态码。相关实现见 exec.rs 中的ExecResponse:exec返回 stdout 的文本与 base64 两种形式(stdout与stdoutB64),exec/stream则按行发出event: stdout,最后以event: exit收尾。
  2. 文件上传必须等到 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 要的是写错的代价
netnetwork机器没有网络;两主机上实测在 create 时被 400 拦下并给出良好报错(image 'alpine' must be pulled from a registry, but this machine has no network...)
memorymemoryMb无任何诊断,静默取默认值(v1.18.2 实测返回"memoryMb":8192)
--之后的 workloadcmd(和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.

项目地址:https://gitcode.com/gh_mirrors/sm/smolvm
点击查看免费下载
上一篇:Blender 3MF插件:让3D打印工作流更高效
下一篇:Wand-Enhancer 使用完全指南:3 步为 WeMod 打本地补丁,解锁 Pro 与远程面板

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 5:08:50

xberg Python SDK 图片提取实战:关闭 OCR 的 PNG 元数据冒烟测试

后端AI 应用NLP 【免费下载链接】xberg Polyglot document intelligence with a Rust core: extract text, metadata, images, tables, and structured data from 106 formats across 140 file extensions, plus code intelligence for 371 languages. Fifteen bindings, with …

作者头像 李华
网站建设 2026/10/9 5:08:10

联合储能的配电网优化调度及新能源消纳评估Matlab实现

最近接了一个项目&#xff0c;标题是“联合储能的配电网优化调度及新能源消纳能力评估研究&#xff08;Matlab代码实现&#xff09;”&#xff0c;光看名字就知道这是个典型的电力方向课题——涉及储能建模、配电网潮流、优化调度、新能源消纳评估&#xff0c;还要求用 Matlab …

作者头像 李华
网站建设 2026/10/9 5:06:51

家具AI生图工具哪个品牌好

在家具行业&#xff0c;产品图片的质量直接关系到客户的购买决策。然而&#xff0c;传统拍摄方式的高成本、低效率&#xff0c;以及后期制作繁琐&#xff0c;长期困扰着工厂、电商和设计师。随着AI技术的渗透&#xff0c;家具AI生图工具应运而生&#xff0c;试图以“一键生成”…

作者头像 李华
网站建设 2026/10/9 5:05:17

uniapp打包微信小程序插件接入全流程与高频踩坑指南

做 uniapp 打包微信小程序这活儿&#xff0c;最磨人的从来不是写页面&#xff0c;而是配置不对、插件接不上、包打出来丢进开发者工具直接白屏。前前后后折腾了二十来个版本&#xff0c;踩了不少坑之后&#xff0c;我把整个流程里该注意的地方都捋了一遍。这篇文章就围绕“unia…

作者头像 李华
网站建设 2026/10/9 5:04:30

Android动漫聚合插件开发实战:插件化架构与解析技巧

1. 从零拆解一个动漫聚合插件的设计逻辑1.1 这个插件到底解决了什么问题Android 上的动漫播放器生态一直有个尴尬的现状&#xff1a;官方应用商店里能上架的播放器&#xff0c;内容源往往少得可怜&#xff0c;更新还慢&#xff1b;而用户真正想看的番剧&#xff0c;散落在各种不…

作者头像 李华