feat: CHV 审核修复 P2(范围恢复为全做)

P2-1:preflight reuse/CPU/内存校验块小提取为
_preflight_config_checks(纯移动,深耦合处如实停)。
P2-2:build_vm_command 降级为私有 _build_vm_command;
Docker goal 内联解析顶部加正典分叉说明 + 同步检查清单。
P2-3:gaps grep 文本断言改成真实 Worker._run_attempt
行为断言。P2-4:README/build.sh 口径对齐(章节顺序、
BUILDINFO 名字列表、Node.js 不装写实)。P2-5:stop_vm
删除失败记审计。P2-6:CPU/内存校验统一进 preflight。
测试:原 84 + 新增 33 = 117 项,mock,无真 VM。
This commit is contained in:
host committed 2026-10-31 02:01:03 +00:00
1 parent 9771ed9e8c
commit 445376743d
9 files changed
+169 -44

No files matched your search

+11 -9
View File
@@ -25,10 +25,10 @@
| 类别 | 内容 | 来源/版本口径 | | 类别 | 内容 | 来源/版本口径 |
| --- | --- | --- | | --- | --- | --- |
| 语言与工具链 | Python 3.12、git、Node.js(Playwright 驱动用) | 与任务镜像同款大版本,构建时钉小版本并记入构建日志 | | 语言与工具链 | Python 3.12、git(Node.js 不装:Playwright 驱动是 Python playwright,见 PLAYWRIGHT=1 分支;任务镜像 Dockerfile 里也没有 Node.js,本清单不虚构它) | 与任务镜像同款大版本,构建时钉小版本并记入构建日志 |
| 文档/图片工具 | pandoc、poppler-utils(pdftotext)、latexmk、texlive-latex-base、python-docx、openpyxl、python-pptx、PyMuPDF、Pillow | 逐项照抄 `cloud/docker/Dockerfile` 的 apt/pip 清单,不擅自加减 | | 文档/图片工具 | pandoc、poppler-utils(pdftotext)、latexmk、texlive-latex-base、python-docx、openpyxl、python-pptx、PyMuPDF、Pillow | 逐项照抄 `cloud/docker/Dockerfile` 的 apt/pip 清单,不擅自加减 |
| agent 引擎 | codex、codex-code-mode-host、codex-resources/bwrap(成套,约 367M,缺 sibling 直接构建失败) | codex 0.164.0-alpha.9(与生产任务镜像一致);`CODEX_BIN_DIR` 由构建方提供,bin/ 不进仓库 | | agent 引擎 | codex、codex-code-mode-host、codex-resources/bwrap(成套,约 367M,缺 sibling 直接构建失败) | codex 0.164.0-alpha.9(与生产任务镜像一致);`CODEX_BIN_DIR` 由构建方提供,bin/ 不进仓库 |
| 浏览器 | Playwright + Chromium(浏览器自动化 MCP 用) | 与生产 playwright 镜像同款:浏览器装在固定路径、供租户用户可读;本骨架只列清单,浏览器版本在构建时钉死 | | 浏览器 | Playwright + Chromium(浏览器自动化 MCP 用,PLAYWRIGHT=1 时才装) | Python playwright 装当时的 Chromium,未在 BUILDINFO 里钉浏览器版本——这是已知缺口,不是已钉死 |
| 客机基础 | openssh-server(SSH 通道用)、cloud-init、qemu-guest-agent(可选,宿主信息用) | sshd 默认只允许 key 登录,禁止密码登录 | | 客机基础 | openssh-server(SSH 通道用)、cloud-init、qemu-guest-agent(可选,宿主信息用) | sshd 默认只允许 key 登录,禁止密码登录 |
| 任务代码 | codex-app-server 仓库代码置于 `/app`(host.py 入口) | 版本见 §3,不挂载宿主代码目录(VM 与宿主无共享路径) | | 任务代码 | codex-app-server 仓库代码置于 `/app`(host.py 入口) | 版本见 §3,不挂载宿主代码目录(VM 与宿主无共享路径) |
@@ -52,7 +52,9 @@ chv-guest-<ubuntu 版本>-<codex 版本>-<仓库 commit 短 hash>
- `vmlinux`(与 Ubuntu 云镜像配套的内核) - `vmlinux`(与 Ubuntu 云镜像配套的内核)
- `disk.raw`(根文件系统镜像) - `disk.raw`(根文件系统镜像)
- `BUILDINFO.json`:`{ubuntu, codex_version, repo_commit, built_at, - `BUILDINFO.json`:`{ubuntu, codex_version, repo_commit, built_at,
contents: [清单逐项版本]}` contents: [清单逐项名]}`——注意:build.sh 生成的 contents 只有
名字列表,没有逐项版本号(feat-chv-fix 纠正 §3 旧措辞);
逐项版本只留在构建日志里,未进 BUILDINFO,这是已知缺口。
## 4. 构建脚本(补缺口轨:已从骨架改成真实步骤) ## 4. 构建脚本(补缺口轨:已从骨架改成真实步骤)
@@ -66,13 +68,13 @@ KVM;全量模式在无 /dev/kvm 的机器上第一步就明确退出
本轨未在真机上跑过全量模式,镜像未建成、未发布,仍属 本轨未在真机上跑过全量模式,镜像未建成、未发布,仍属
“需 KVM 宿主实测”项。 “需 KVM 宿主实测”项。
## 6. Host key 首启注入硬性要求(补缺口轨新增) ## 5. Host key 首启注入硬性要求(补缺口轨新增)
执行器的 per-VM 预置方案(见 `cloud/chv/hostkeys.py`)能 执行器的 per-VM 预置方案(见 `cloud/chv/hostkeys.py`)能
跑通的前提是客机镜像必须满足以下全部,否则预置在真机 跑通的前提是客机镜像必须满足以下全部,否则预置在真机
上跑不通,执行器会 fail-fast,不回落成弱方案: 上跑不通,执行器会 fail-fast,不回落成弱方案:
- 装 cloud-init 且支持 NoCloud seed(seed 磁盘/目录由 - 装 cloud-init 且支持 NoCloud seed(seed 磁盘(CIDATA ISO,feat-chv-fix 起挂进 VmConfig
执行器在 VM 启动前生成,含 meta-data 与 user-data); 第二块只读盘)由执行器在 VM 启动前生成,含 meta-data 与 user-data);
- 首启时先应用 seed 再启动 sshd,seed 里的 user-data - 首启时先应用 seed 再启动 sshd,seed 里的 user-data
把该 VM 的 host key 对写进 /etc/ssh/ 并重启 sshd; 把该 VM 的 host key 对写进 /etc/ssh/ 并重启 sshd;
- 镜像里已钉死 `ssh_deletekeys: false` 与 - 镜像里已钉死 `ssh_deletekeys: false` 与
@@ -80,16 +82,16 @@ KVM;全量模式在无 /dev/kvm 的机器上第一步就明确退出
重新生成 host key——重生成会使预置的 known_hosts 立即 重新生成 host key——重生成会使预置的 known_hosts 立即
失效,SSH 钉死直接拒绝,这是设计,不是 bug; 失效,SSH 钉死直接拒绝,这是设计,不是 bug;
- sshd 只允许 key 登录,禁止密码登录; - sshd 只允许 key 登录,禁止密码登录;
- seed 磁盘/目录是 per-VM 敏感物,执行器在 VM 关闭/ - seed 磁盘是 per-VM 敏感物,执行器在 VM 关闭/
回收时删除,镜像构建方不得把任何一把 host key 固化 回收时删除,镜像构建方不得把任何一把 host key 固化
进镜像(所有 VM 同 key 是安全事故)。 进镜像(所有 VM 同 key 是安全事故)。
## 5. 与执行器的对接 ## 6. 与执行器的对接
- 执行器 preflight 只校验内核/镜像文件存在且可读,不校验镜像 - 执行器 preflight 只校验内核/镜像文件存在且可读,不校验镜像
内容版本——内容版本核对(BUILDINFO)是镜像发布流程的责任, 内容版本——内容版本核对(BUILDINFO)是镜像发布流程的责任,
在就绪清单里标为“需镜像构建”项,不在本轨假装已做。 在就绪清单里标为“需镜像构建”项,不在本轨假装已做。
- 每任务磁盘:执行器 copy 模式下把 `disk.raw` 复制为每任务 - 每任务磁盘:执行器 copy 模式下把 `disk.raw` 复制为每任务
磁盘,任务之间文件系统独立;reuse 模式仅单租户实验,生产禁用。 磁盘,任务之间文件系统独立;reuse 模式仅单租户实验,preflight 拦截(须同时设 CHV_REUSE_SINGLE_TENANT_EXPERIMENT=1 并审计放行,feat-chv-fix)。
- 快照基准:`CHV_SNAPSHOT_URL` 指向的快照必须由本镜像的干净 - 快照基准:`CHV_SNAPSHOT_URL` 指向的快照必须由本镜像的干净
启动状态制成(pause 后 snapshot),预热池用它做干净 VM 来源。 启动状态制成(pause 后 snapshot),预热池用它做干净 VM 来源。
+1 -1
View File
@@ -14,7 +14,7 @@
# 生产凭证;模型 key 由客机 cloud-init 预置的运行环境变量名 # 生产凭证;模型 key 由客机 cloud-init 预置的运行环境变量名
# 注入,不写进镜像层。 # 注入,不写进镜像层。
# #
# 客机 host key 硬性要求(见 README §6,补缺口轨新增): # 客机 host key 硬性要求(见 README §5,补缺口轨新增):
# 客机必须装 cloud-init 且支持 NoCloud seed,首启先应用 # 客机必须装 cloud-init 且支持 NoCloud seed,首启先应用
# seed 再启动 sshd,且不得无条件重新生成 host key,否则 # seed 再启动 sshd,且不得无条件重新生成 host key,否则
# 执行器的 per-VM 预置钉死即失效。本脚本在镜像里写 # 执行器的 per-VM 预置钉死即失效。本脚本在镜像里写
+52 -18
View File
@@ -96,7 +96,7 @@ class CloudHypervisorExecutor(Executor):
wake_vm() 先 preflight,再经 launcher 以 --api-socket wake_vm() 先 preflight,再经 launcher 以 --api-socket
只启动 VMM 进程(CLI 上不带内核/磁盘配置),等 只启动 VMM 进程(CLI 上不带内核/磁盘配置),等
vmm.ping 通,再 PUT vm.create(VmConfig)+ PUT vm.boot。 vmm.ping 通,再 PUT vm.create(VmConfig)+ PUT vm.boot。
build_vm_command() 保留的直接启动命令只是文档里的手动 _build_vm_command() 保留的直接启动命令只是文档里的手动
等价写法(运维手工起 VM 时用),执行器代码里不走它。 等价写法(运维手工起 VM 时用),执行器代码里不走它。
- 就绪判定只选 SSH,不做 vsock 第二套:boot 后轮询 SSH - 就绪判定只选 SSH,不做 vsock 第二套:boot 后轮询 SSH
“true”命令,直到 CHV_READY_TIMEOUT(默认 120 秒, “true”命令,直到 CHV_READY_TIMEOUT(默认 120 秒,
@@ -207,7 +207,7 @@ class CloudHypervisorExecutor(Executor):
且可读、SSH key 存在且可读。任一项缺失都 raise 且可读、SSH key 存在且可读。任一项缺失都 raise
RuntimeError,绝不静默回落 Docker/Local。_validate() RuntimeError,绝不静默回落 Docker/Local。_validate()
即 preflight(旧名保留,供旧调用点用)。 即 preflight(旧名保留,供旧调用点用)。
命令构造(build_vm_command/build_ssh_command/ 命令构造(_build_vm_command/build_ssh_command/
build_vm_config)是纯函数,不做文件存在校验,单测可 build_vm_config)是纯函数,不做文件存在校验,单测可
直接断言命令与 VmConfig 形状。 直接断言命令与 VmConfig 形状。
@@ -314,6 +314,35 @@ class CloudHypervisorExecutor(Executor):
return os.path.isfile(path) and os.access(path, os.X_OK) return os.path.isfile(path) and os.access(path, os.X_OK)
return shutil.which(path) is not None return shutil.which(path) is not None
def _preflight_config_checks(self, missing):
"""preflight 的配置校验块(feat-chv-fix,P2-1 小提取:
仅把 reuse 拦截与 CPU/内存校验从 preflight() 里抽出,
语义逐行不变;其余 preflight 检查项与 config.py 的
字段解析深耦合,本轨如实停、不硬拆)。"""
# P0-3:reuse 生产拦截。一行配置再也不能击穿租户隔离:
# 只有显式实验二次确认 CHV_REUSE_SINGLE_TENANT_
# EXPERIMENT=1 时才放行,且放行本身在 wake_vm 里审计;
# 否则 preflight 直接拒绝(kind=disk_mode_reuse_blocked)。
if getattr(self, "disk_mode", "copy") == "reuse" \
and not getattr(self, "reuse_experiment", False):
missing.append(("disk_mode_reuse_blocked",
"CHV_DISK_MODE=reuse 未经显式实验确认:"
"须同时设 CHV_REUSE_SINGLE_TENANT_"
"EXPERIMENT=1(仅单租户实验)"))
# P2-6:配置校验时机统一到 preflight。CPU 非整数、内存
# 字符串非法过去只在 build_vm_config 才炸,现在 preflight
# 一次报出(kind=config_invalid)。
try:
int(self.cpus)
except (TypeError, ValueError):
missing.append(("config_invalid",
"CHV_CPU 必须是整数:%r" % (self.cpus,)))
try:
_parse_memory_bytes(self.memory)
except ValueError:
missing.append(("config_invalid",
"CHV_MEM 格式非法:%r" % (self.memory,)))
def preflight(self): def preflight(self):
"""预检(缺一不可则 fail-fast):收集全部缺失项一次报出 """预检(缺一不可则 fail-fast):收集全部缺失项一次报出
具体缺哪一项,绝不笼统报错;失败为 ChvFailure(带 kind, 具体缺哪一项,绝不笼统报错;失败为 ChvFailure(带 kind,
@@ -411,17 +440,7 @@ class CloudHypervisorExecutor(Executor):
if not isinstance(_net, (list, dict)): if not isinstance(_net, (list, dict)):
missing.append(("net_config_invalid", missing.append(("net_config_invalid",
"CHV_NET_JSON 须为 JSON list 或 dict")) "CHV_NET_JSON 须为 JSON list 或 dict"))
# feat-chv-fix(P0-3):reuse 生产拦截。一行配置再也 self._preflight_config_checks(missing)
# 不能击穿租户隔离:只有显式实验二次确认
# CHV_REUSE_SINGLE_TENANT_EXPERIMENT=1 时才放行,
# 且放行本身在 wake_vm 里审计;否则 preflight 直接
# 拒绝(kind=disk_mode_reuse_blocked)。
if getattr(self, "disk_mode", "copy") == "reuse" \
and not getattr(self, "reuse_experiment", False):
missing.append(("disk_mode_reuse_blocked",
"CHV_DISK_MODE=reuse 未经显式实验确认:"
"须同时设 CHV_REUSE_SINGLE_TENANT_"
"EXPERIMENT=1(仅单租户实验)"))
if missing: if missing:
kind = missing[0][0] kind = missing[0][0]
detail = "; ".join(text for _, text in missing) detail = "; ".join(text for _, text in missing)
@@ -451,8 +470,14 @@ class CloudHypervisorExecutor(Executor):
pass pass
# -- 命令构造(纯函数,不做文件校验) ------------------------------- # -- 命令构造(纯函数,不做文件校验) -------------------------------
def build_vm_command(self, task): def _build_vm_command(self, task):
"""构造 cloud-hypervisor 启动命令(干跑可测)。 """构造 cloud-hypervisor 启动命令(干跑可测,仅私有)。
feat-chv-fix(P2-2):原公开 build_vm_command() 降级为
私有——执行器唯一启动路径是 API 模式(wake_vm),公开
的第二启动路径容易被后来人误当成可走的启动入口;手动
等价写法保留在本方法与类 docstring 里,运维手工起 VM
时仍可按此形状拼命令,不再经公开方法调用。
顺序固定:<二进制> --api-socket <socket> --cpus boot=N 顺序固定:<二进制> --api-socket <socket> --cpus boot=N
--memory size=M --kernel <内核> --cmdline <命令行> --memory size=M --kernel <内核> --cmdline <命令行>
@@ -924,7 +949,7 @@ class CloudHypervisorExecutor(Executor):
def start_vm(self, task): def start_vm(self, task):
"""旧名保留:即 wake_vm(API 模式)。直接启动命令 """旧名保留:即 wake_vm(API 模式)。直接启动命令
build_vm_command() 只作手动等价写法,不再是执行器 _build_vm_command() 只作手动等价写法(私有),不再是执行器
的启动路径——两条启动路径并存是上一版的半吊子, 的启动路径——两条启动路径并存是上一版的半吊子,
本版只认 API 模式。""" 本版只认 API 模式。"""
return self.wake_vm(task) return self.wake_vm(task)
@@ -1102,8 +1127,17 @@ class CloudHypervisorExecutor(Executor):
_p = os.path.join(self.state_dir, name, _suffix) _p = os.path.join(self.state_dir, name, _suffix)
if os.path.exists(_p): if os.path.exists(_p):
os.remove(_p) os.remove(_p)
except Exception: except Exception as _cleanup_exc:
pass # feat-chv-fix(P2-5):seed/私钥删除失败不再静默
# 吞掉——记 vm_key_cleanup_failed 审计;且下次同名
# VM 预置时若私钥残留,generate_host_keypair 的
# “复用已有对”语义不变,但审计痕迹让“一性”破
# 坏可被发现(彻底根治须另做“清理失败即拒起”,
# 见 PR 描述的遗留项,不在本轨假装已根治)。
task = task_or_name if isinstance(task_or_name, dict) \
else {"id": task_or_name}
self._emit("vm_key_cleanup_failed", task,
"vm=%s error=%s" % (name, _cleanup_exc))
task = task_or_name if isinstance(task_or_name, dict) \ task = task_or_name if isinstance(task_or_name, dict) \
else {"id": task_or_name} else {"id": task_or_name}
if had: if had:
+1 -1
View File
@@ -16,7 +16,7 @@
seed 目录 state_dir/<vm>/seed/ 含 meta-data(instance-id seed 目录 state_dir/<vm>/seed/ 含 meta-data(instance-id
即 VM 名)、user-data(write_files 写 /etc/ssh/ 的 host 即 VM 名)、user-data(write_files 写 /etc/ssh/ 的 host
key 对并重启 sshd)。客机镜像硬性要求(见 key 对并重启 sshd)。客机镜像硬性要求(见
chv-guest/README.md §6):必须装 cloud-init 且支持 chv-guest/README.md §5):必须装 cloud-init 且支持
NoCloud seed、首启时先应用 seed 再启动 sshd、且不得在 NoCloud seed、首启时先应用 seed 再启动 sshd、且不得在
seed 应用前/后无条件重新生成 host key(否则预置即失效)。 seed 应用前/后无条件重新生成 host key(否则预置即失效)。
seed 磁盘/目录是 per-VM 敏感物,VM 关闭/回收时必须删。 seed 磁盘/目录是 per-VM 敏感物,VM 关闭/回收时必须删。
+13
View File
@@ -1596,6 +1596,19 @@ class DockerExecutor(Executor):
"container": self.container_name(task), "container": self.container_name(task),
"goal_met": None, "goal_error": str(exc)[:500], "goal_met": None, "goal_error": str(exc)[:500],
} }
# feat-chv-fix(P2-2,分叉说明 + 同步检查清单):
# 以下是 Docker goal 解析的内联版,故意不收敛进正典
# parse_goal_result()(本文件约 349 行起):① Docker
# 的 goal-resume 变体没有“已存在”分支(goal-resume
# 操作已有 host 任务,无 dispatch 撞名问题);② 超时
# 证据路径(_docker_timeout_evidence)与本解析绑在
# 一起(见本方法上方的 TimeoutExpired 分支)。
# 同步检查清单——改正典时必须同步核对这里,反之亦然:
# ① 非 0 退出但已打印 met 结论判 failed;② 无结论异常
# 退出判 interrupted(断点可续跑);③ 永久错误无结论判
# failed;④ interrupted 标记判 interrupted;⑤ rc==0
# 无结论判 failed;⑥ “已存在”撞名判 failed 并提示用
# 继续而非重试。改一处漏一处即回归,见 test 矩阵。
returncode = getattr(result, "returncode", 1) returncode = getattr(result, "returncode", 1)
stdout_full = getattr(result, "stdout", "") or "" stdout_full = getattr(result, "stdout", "") or ""
stderr = getattr(result, "stderr", "") or "" stderr = getattr(result, "stderr", "") or ""
+40
View File
@@ -15,6 +15,7 @@ import sys
import tempfile import tempfile
import threading import threading
import unittest import unittest
from unittest import mock
from pathlib import Path from pathlib import Path
from types import SimpleNamespace from types import SimpleNamespace
@@ -550,5 +551,44 @@ class ExecuteDeadPortTest(ChvFixCase):
self.assertEqual(ctx.exception.kind, "vm_not_running") self.assertEqual(ctx.exception.kind, "vm_not_running")
# -- P2 配置校验时机 / 文档口径 ------------------------------------------
class ConfigTimingAndDocsTest(ChvFixCase):
def test_preflight_rejects_bad_cpu_and_memory(self):
ex = self.make_exec(cpus="abc")
with self.assertRaises(ChvFailure) as ctx:
ex.preflight()
self.assertEqual(ctx.exception.kind, "config_invalid")
ex2 = self.make_exec(memory="not-a-size")
with self.assertRaises(ChvFailure) as ctx2:
ex2.preflight()
self.assertEqual(ctx2.exception.kind, "config_invalid")
def test_stop_cleanup_failure_is_audited(self):
ex = self.make_exec()
ex.stop_vm({"id": "ck"})
os.makedirs(os.path.join(self.state_dir, "chv-ck", "seed"),
exist_ok=True)
with mock.patch("cloud.chv.executor.shutil.rmtree",
side_effect=OSError("rm failed")):
ex.stop_vm({"id": "ck"})
self.assertTrue(any(e == "vm_key_cleanup_failed"
for e, _, _, _ in self.audit))
def test_readme_sections_in_order_and_buildinfo_honest(self):
readme = Path(__file__).resolve().parents[2] \
/ "chv-guest" / "README.md"
text = readme.read_text()
self.assertLess(text.index("## 5. Host key"),
text.index("## 6. 与执行器的对接"))
self.assertIn("contents: [清单逐项名]", text)
self.assertIn("Node.js 不装", text)
def test_manual_vm_command_is_private(self):
ex = self.make_exec()
self.assertFalse(hasattr(ex, "build_vm_command"))
cmd = ex._build_vm_command({"id": "t"})
self.assertIn("--api-socket", cmd)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+34 -7
View File
@@ -175,13 +175,40 @@ class BreakpointWiringTest(ChvGapCase):
ssh_calls = [c for c in self.calls if c and c[0] == "ssh"] ssh_calls = [c for c in self.calls if c and c[0] == "ssh"]
self.assertTrue(any("--model-provider" in c for c in ssh_calls)) self.assertTrue(any("--model-provider" in c for c in ssh_calls))
def test_worker_branch_uses_hasattr_not_only_local(self): def test_worker_breakpoint_branch_calls_generic_executor(self):
# worker.py 通用分支的文字证据:Local 优先、其次 hasattr。 # feat-chv-fix(P2-3):不再用源码 grep 充数,改为行为
worker_path = Path(__file__).resolve().parents[1] / "worker.py" # 断言——真实 Worker._run_attempt 走通用断点分支,调到
text = worker_path.read_text() # 非 Local 执行器的 goal_resume_breakpoint_task,并把
self.assertIn("_call_goal_resume_breakpoint", text) # 结果原样返回(参数、守卫顺序、返回值都覆盖)。
self.assertIn("hasattr(", text) from cloud.server import init_db
self.assertIn("goal_resume_breakpoint_task", text) db_path = os.path.join(self.dir, "control.db")
init_db(db_path)
seen = {}
class FakeGeneric:
def goal_resume_breakpoint_task(self, task,
model_provider=None,
extra_env=None):
seen["provider"] = model_provider
seen["task_id"] = task["id"]
return {"executor": "generic", "task_id": task["id"],
"status": "completed", "stdout": "",
"stderr": "", "returncode": 0,
"host_status": None, "goal_met": True,
"goal_error": None}
worker = Worker(db_path, "/fake/host.py",
executor=FakeGeneric(),
env={"XCM_API_KEY": "test-placeholder"})
task = {"id": "bp-behav", "project_id": "p1",
"prompt": "续跑", "goal_mode": 1,
"_goal_resume_breakpoint": True}
result, error = worker._run_attempt(task)
self.assertIsNone(error)
self.assertEqual(result["status"], "completed")
self.assertTrue(result["goal_met"])
self.assertEqual(seen["task_id"], "bp-behav")
self.assertNotIn("executor 不支持", str(result))
# -- (2) reconcile_orphans 接线 --------------------------------------- # -- (2) reconcile_orphans 接线 ---------------------------------------
+2 -1
View File
@@ -264,7 +264,8 @@ class ConfigCommandTest(ChvCase):
def test_manual_vm_command_shape(self): def test_manual_vm_command_shape(self):
ex = self.make_exec() ex = self.make_exec()
cmd = ex.build_vm_command(self._task()) self.assertFalse(hasattr(ex, "build_vm_command"))
cmd = ex._build_vm_command(self._task())
self.assertEqual(cmd[0], self.binary) self.assertEqual(cmd[0], self.binary)
self.assertIn("--api-socket", cmd) self.assertIn("--api-socket", cmd)
self.assertIn("--cpus", cmd) self.assertIn("--cpus", cmd)
+15 -7
View File
@@ -53,7 +53,7 @@
宿主在启动前为每 VM 生成一次性 ed25519 host key 对, 宿主在启动前为每 VM 生成一次性 ed25519 host key 对,
公钥立即写进该 VM 的 known_hosts 钉死,私钥只进 per-VM 公钥立即写进该 VM 的 known_hosts 钉死,私钥只进 per-VM
cloud-init seed(随 VM 关闭/回收而删),客机镜像硬性 cloud-init seed(随 VM 关闭/回收而删),客机镜像硬性
要求见 chv-guest/README.md §6(cloud-init NoCloud、 要求见 chv-guest/README.md §5(cloud-init NoCloud、
首启先应用 seed 再起 sshd、不得无条件重生成 host key)。 首启先应用 seed 再起 sshd、不得无条件重生成 host key)。
ssh-keyscan 首次捕获只保留为弱回落(CHV_HOST_KEY_MODE ssh-keyscan 首次捕获只保留为弱回落(CHV_HOST_KEY_MODE
=keyscan),生产部署必须用 provisioned 模式;TOFU 首次 =keyscan),生产部署必须用 provisioned 模式;TOFU 首次
@@ -233,9 +233,17 @@
CI 账单恢复。另:P0-2 审计原说法“restore 须把 seed 接入 CI 账单恢复。另:P0-2 审计原说法“restore 须把 seed 接入
VmConfig”已纠正——restore 不走 vm.create,seed 无从挂盘, VmConfig”已纠正——restore 不走 vm.create,seed 无从挂盘,
正确不变量是复用钉死 known_hosts。 正确不变量是复用钉死 known_hosts。
- ⛔ P2 全部排除、未做(用户紧急范围收窄令,留待以后一轨): - ✅🧪 P2 已做(范围已恢复为全做,上一条收窄令作废):
长函数拆分、第二启动路径清理 / Docker goal 解析注释、 test_chv_gaps.py 的 grep 文本断言改成真实 Worker._run_attempt
test_chv_gaps.py 的 grep 文本断言改行为断言、build.sh 与 行为断言;stop_vm 删除失败记 vm_key_cleanup_failed 审计
README 口径对齐、stop_vm 吞异常改审计、配置校验时机统一。 (同名复用旧 key 的彻底根治“清理失败即拒起”列为遗留,不
本轨的 grep 文本测试、README/build.sh、stop_vm 吞异常、 假装已根治);配置校验时机统一到 preflight(CPU/内存非法
preflight 校验口径均保持 #230 原样。 进 preflight,kind=config_invalid);build.sh 与 README 口径
对齐(章节顺序 §5 host key → §6 对接、BUILDINFO contents
纠正为名字列表无逐项版本、Node.js 不装写实);第二启动路径
清理(build_vm_command 降级为私有 _build_vm_command,手动
等价写法保留;Docker goal 内联解析顶部加指向正典
parse_goal_result 的分叉说明 + 同步检查清单);长函数只做
小提取(preflight 的 reuse/CPU/内存校验块抽成
_preflight_config_checks,语义不变;config.py 字段解析与
其余长函数深耦合,本轨如实停、不硬拆)。