关于 upgrade-vm
upgrade-vm 是用于升级 IV 虚拟机的操作技能,面向维护该虚拟机群的操作者。默认方式是在原机上运行指定版本的配置脚本;仅在需要全新磁盘或更新基础镜像时重建,并包含验证与连接纪律。
主要功能
- 01在原机上运行指定版本的配置脚本
- 02迁移旧版检出目录
- 03检查 Shelley 版本与锁定文件
- 04按顺序执行 SSH 操作
- 05必要时销毁并重建虚拟机
适用场景
- 01将 IV 虚拟机升级到指定版本
- 02迁移使用旧目录的虚拟机
- 03验证配置结果及 Shelley 服务版本
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ kylelundstedt/ dotfiles --skill upgrade-vm安装后在 Claude Code 中输入/upgrade-vm确认已加载。
详细介绍
升级 VM
IV VM 以 ghcr.io/kylelundstedt/exeslim-dev 为基础镜像(通过 LABEL exe.dev/install-shelley=true 和自带的单元保留 Shelley),并从 iv-provision 仓库中的 provision-iv.sh 获取工具。因此,“升级”VM 通常只需使用更新的提交重新运行配置脚本——原地升级,无需重建、擦除磁盘或更改 tailnet。
彻底销毁并重建现在属于例外情况,仅当你确实需要全新磁盘,或 exe.dev 管理的基础镜像发生了无法通过重新配置获取的变更时,才需要这样做。
路径 A——原地重新配置(默认)
用户提供 VM 名称和 iv-provision 仓库的目标发布标签。
每次只执行一条 SSH 命令——绝不并行执行 SSH。
通过 tailnet 连接 VM:使用 ssh <vm>,而不是 <vm>.exe.xyz
Fleet VM 是 tailnet 节点,因此可以直接通过 Tailscale SSH 连接,无需密钥。exe.dev 边缘入口(ssh <vm>.exe.xyz)是 VM 尚未加入 tailnet 时使用的引导路径——它需要 exe.dev SSH 密钥,而 VM 并没有此密钥,因此从另一台 VM 连接时会直接失败。仅在路径 B 的加入 tailnet 前使用它。
始终指定明确的标签;切勿使用 --detach main
git checkout --detach main 会解析本地的 main;在 fleet VM 上,它指向上次配置时获取的版本——通常已经过时数月。该命令会成功执行,不会打印任何明显的警告,却会配置较旧的配方。请指定标签。
从 3.0.8 起,配置脚本会打印正在使用的提交版本,并在当前检出版本落后于上游时发出警告。每次运行都要查看这一行;它是避免因拼写错误而悄然降级的唯一保障。
ssh <vm> "cd ~/iv-provision \
&& git fetch --tags --quiet \
&& git checkout --detach <tag> \
&& ~/iv-provision/provision-iv.sh \
&& ~/iv-provision/tests/smoke-provision.sh ~/iv-provision"
迁移早于 3.0.0 的 VM(~/iv-image)
重命名前配置的 VM,其检出目录位于 ~/iv-image,且 origin 指向旧名称。删除该目录并重新克隆,不要重命名目录并改写远程地址:该检出目录是可随时丢弃的产物——不包含任何 VM 状态,它生成的所有内容都存放在别处(~/iv-provision.lock、~/.local/bin、~/.agents)——因此,重新克隆涉及的变更更少,也不会遗留迁移不完整的远程地址、过时的分离 HEAD,或无人打算保留的本地修改。
ssh <vm> "rm -rf ~/iv-image \
&& git clone --quiet https://github.com/kylelundstedt/iv-provision.git ~/iv-provision \
&& git -C ~/iv-provision checkout --detach <tag> \
&& ~/iv-provision/provision-iv.sh \
&& ~/iv-provision/tests/smoke-provision.sh ~/iv-provision"
从公开的 github.com 克隆,不要使用 github.int.exe.xyz:旧的 repo-iv-image 集成指向的仓库名称已无法解析,并会返回 HTTP 403。不要假设 VM 使用了哪个远程地址,先通过 git -C ~/iv-image remote -v 检查。
如果 VM 的 ~/iv-image 中有本地提交,请停止并检查——没有哪个 VM 工作树具备权威性,但也不应悄然丢弃已有工作。
这会重新固定工具版本、重新安装 vendored skills 和 Agent 配置,并重写 ~/iv-provision.lock。请验证:
ssh <vm> "cat ~/iv-provision.lock"
间隔几分钟,两次验证 Shelley 固定版本
配置日志干净并不能证明一切正常。kgl-songs 在 2026-08-17 报告已安装 0.959,但六十秒后又开始提供较旧的构建版本,这是由套接字激活竞态导致的(已在 3.0.1 中修复)。立即比较二进制文件与实际响应套接字的版本,然后几分钟后再比较一次:
ssh <vm> "shelley version | jq -r .version; \
curl -fsS -H 'X-Exedev-Userid: probe' \
--unix-socket ~/.config/shelley/shelley.sock \
http://localhost/version | jq -r .version"
两者都必须与固定版本一致。对于已使用该固定版本的 VM,预期不会重启 Shelley(已启用固定版本及自更新覆盖;不会重启),也不会降级 Claude Code——claude 和 codex 是最低版本要求,而不是精确固定版本。如果出现任一情况,请停止并调查,不要继续处理 fleet 中的其他 VM。
每台 VM 处理完后,还要检查 ~/iv-provision.lock:python3_version、tailscale_version、entire_version、entire_plugin_version 和 skills_count 都应有值。
在 Claude Code 和 Codex 中各登录一次(Aperture 版本)
从 Aperture 版本开始,两个 CLI 都会通过
http://ai.dojo-sun.ts.net 使用 VM 自身的订阅登录信息,因此没有这些登录信息的
VM 会报错:Codex 会从 .../codex/responses 返回 401 Unauthorized,Claude Code 会显示 Not logged in。登录信息保存在主目录中,因此后续原地升级后仍会保留;重新创建的 VM(路径 B)需要重新登录。先检查:
ssh <vm> "ls ~/.codex/auth.json ~/.claude/.credentials.json"
对于每个缺少登录信息的工具,所有者都需要在浏览器中批准:
- **Codex:**运行
ssh <vm> 'codex login --device-auth',会显示一个 URL 和一次性 验证码;在https://auth.openai.com/codex/device批准。 - **Claude Code:**仅支持交互式登录:运行
ssh <vm>,再运行claude,然后输入/login。
然后确认两者都能通过网关响应,且未覆盖模型:
ssh <vm> 'cd /tmp && claude -p "Reply with exactly: ok" \
&& codex exec --skip-git-repo-check "Reply with exactly: ok"'
如果此处出现模型错误(例如提示 claude-opus-5[1m]“may not exist”),说明 CLI 的
默认模型不在 Aperture 的提供商列表中:固定的模型版本落后于
网关。不要在每台 VM 上单独绕过此问题;应更新固定版本。
最后处理生产 VM
运行着他人依赖服务的 VM(kgl-songs、telnyx-vm)要最后逐台处理,并在之后确认其服务仍在正常运行。
如果 ~/iv-provision 尚不存在(较旧的 VM),请先克隆它——参见 bootstrap.md。
现在无需重新运行个人覆盖配置即可确保正确性。 从
3.0.0 起,provision-iv.sh 会合并 ~/.claude/settings.json,保留团队文件未定义的钩子事件——因此,个人覆盖配置中的 SessionStart 自动刷新钩子会在配置后继续生效。
过去该脚本会覆盖文件,悄悄删除该钩子,因此需要执行此步骤进行修复。但这一补救措施实际上并未奏效:iv-foundry-stage2 于
2026-08-17 完成配置,2026-08-18 检查时发现 .hooks.SessionStart 缺失,个人覆盖配置仍已安装,而且无人察觉——钩子脚本仍然存在,因此看起来一切正常。它也永远无法自行修复,因为负责恢复它的刷新钩子恰恰就是被删除的内容。
如果想刷新个人覆盖配置自身的内容(拉取其 ~/dotfiles 检出内容、安装新的个人技能),仍值得运行:
ssh <vm> "cd ~/dotfiles && git pull --ff-only && ./install.sh"
个人 Mac:也升级其 CLI(仅限 dotfiles 用户)
这不属于团队流程——如果你没有在 Mac 上使用个人 dotfiles,可跳过此步骤。dotfiles 的 install.sh 不会升级已经安装的 CLI,因此单纯重新运行脚本会使 Mac 落后于本次版本发布刚刚提高的设备最低版本要求——而 Mac 也通过同一个 Aperture 网关连接,因此版本落后时会遇到相同的模型错误。整个设备群完成后,在每台 Mac(klundstedt-mini、klundstedt-mbp)上运行:
cd ~/dotfiles && git pull --ff-only && ./install.sh --upgrade
可以远程处理 mini。它的 22 端口仅对 tag:dev 开放,因此要通过 dev VM 连接;没有终端时,install.sh 会跳过 1Password 和 Keychain 密钥刷新,并运行其他所有内容(dotfiles 2026-10-10):
ssh exedev@<dev-vm> 'ssh klundstedt@klundstedt-mini "cd ~/dotfiles && git pull --ff-only && ./install.sh --upgrade"'
已于 2026-10-10 验证:通过这种方式可升级 Claude Code 和 Codex,这正是 Aperture 所需的部分。从 GitHub 版本安装的工具(gh、
jq、rg、fzf 及其他约十种工具)不会升级:其版本查询会以匿名方式运行,因为 gh 的令牌位于已锁定的 Keychain 中,所以会触发 GitHub 每小时 60 次的 API 限制,并保留当前版本。不会删除任何内容。Claude Code 的 /login、密钥刷新和这些工具的升级,仍需在 mini 自己的终端中运行:其登录用 Keychain 在 SSH 会话中处于锁定状态。
路径 B — 完全销毁并重建(仅在必要时)
这会清除 VM 的本地磁盘——它会重新配置,不会迁移状态。仅在路径 A 无法完成更改时使用。
按顺序执行。一次只运行一条 SSH 命令。
1. 与用户确认
此操作具有破坏性。请确认 VM 名称,并确认可以清除其磁盘。
2. 销毁旧 VM
创建替代 VM 之前,先销毁旧 VM。之后仍可通过主机名找到过期的 Tailscale 节点,并且必须在新 VM 加入之前将其删除。
ssh -o ConnectTimeout=30 exe.dev rm <vm>
3. 删除过期的 Tailscale 节点
在替换节点加入前先删除旧节点,否则新 VM 会得到 -1 后缀。此工作站专用路径需要使用 1Password CLI 和 Industry Vault 账户。从 Tailscale OAuth 客户端获取一个有效期较短(1h)的访问令牌——旧的静态 API 密钥已撤销(2026-07);OAuth 设置请参阅 tailnet.md。凭据通过 stdin 传入 curl 配置,而不是暴露在 curl 的进程参数中:
TS_TOKEN=$(printf 'user = "%s:%s"\n' \
"$(op read 'op://Employee/Tailscale OAuth Dev/Client ID' --account industryvault.1password.com)" \
"$(op read 'op://Employee/Tailscale OAuth Dev/Client secret' --account industryvault.1password.com)" \
| curl --config - -fsS -d grant_type=client_credentials \
https://api.tailscale.com/api/v2/oauth/token \
| jq -er .access_token)
curl_with_tailscale_auth() {
printf 'header = "Authorization: Bearer %s"\n' "$TS_TOKEN" \
| curl --config - "$@"
}
NODE_ID=$(curl_with_tailscale_auth -fsSL \
https://api.tailscale.com/api/v2/tailnet/-/devices \
| jq -er --arg hostname "<vm>" \
'[.devices[] | select(.hostname == $hostname) | .id] | unique | if length == 1 then .[0] else error("expected exactly one matching node") end')
curl_with_tailscale_auth -fsSL -X DELETE \
"https://api.tailscale.com/api/v2/device/$NODE_ID"
unset TS_TOKEN
4. 清除过期的 SSH 状态
ssh-keygen -R <vm> 2>/dev/null || true
ssh -O exit <vm> 2>/dev/null || true
ssh -O exit <vm>.exe.xyz 2>/dev/null || true
5. 创建新 VM(IV dev 基础映像)
exe.dev VM 标签 iv 尚未连接到集成附件机制(截至 2026-08-19,只有 iv-provision 带有此标签,且没有任何内容会按此标签附加),因此仍需在步骤 7 显式附加 api-tailscale。请注意,iv 是 exe.dev 标签,与 Tailscale 的 tag:dev 无关,也不是配置仓库的发布标签。请参阅 tailnet.md →“两套标签系统,同一个词”。
重新创建也是获取较新基础映像的唯一方式——exe.dev 会在 VM 创建时固定其映像。从软件包页面获取当前不可变的构建 ID。此处特意使用构建 ID,而不是可变标签:这条路径的重点是使用一个已知的基础映像,而 exe.dev 对可变标签的缓存最长可达 24 h(包括 :<date> 和 :<sha>——只有 latest/main/master 的缓存时间为 1 h)。
ssh -o ConnectTimeout=30 exe.dev new --name=<vm> \
--image=ghcr.io/kylelundstedt/exeslim-dev:<date>.<run>.<attempt>
如果用户指定了集成,请附加它们:
ssh -o ConnectTimeout=30 exe.dev integrations attach <integration> vm:<vm>
6. 等待 VM 启动
等待约 20s,然后尝试通过一次 SSH 连接,并设置 ConnectTimeout=30:
sleep 20
ssh -o ConnectTimeout=30 <vm>.exe.xyz echo "VM is up"
如果失败,等待 30-60s 后再尝试一次。
7. 重新加入 tailnet 并进行配置
使用 join-tailnet 技能,然后按照 bootstrap.md 执行完整的启动流程(附加 github-kylelundstedt-iv-provision、克隆、运行 provision-iv.sh,然后克隆工作仓库并运行 provision-docsite)。
8. 验证
tailscale status | grep <vm>
ssh <vm> "cat ~/iv-provision.lock"
SSH 使用规范
- 一次只进行一次 SSH 尝试。 切勿并行启动 SSH。
- 等待每条命令完成后再开始下一条。
- 如果 SSH 失败,等待 30-60s 后再尝试一次。
- 对 tailnet 上的所有操作使用
ssh <vm>(Tailscale SSH)。<vm>.exe.xyz仅用于引导,且无法从另一台 VM 访问。 - 从 3.0.9 开始,配置器会写入一个按 tailnet 成员身份(
tailscale ip -4 %h)而不是iv-*名称前缀匹配的 ssh 配置块,因此ssh kgl-songs和ssh telnyx-vm无需任何标志即可工作。对于在 3.0.9 之前完成配置的 VM,请传入-o StrictHostKeyChecking=accept-new -o User=exedev,直至重新配置。