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:
1 parent
9771ed9e8c
commit
445376743d
9 files changed
+169
-44
No files matched your search
+11
-9
@@ -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 清单,不擅自加减 |
|
||||
| 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 登录,禁止密码登录 |
|
||||
| 任务代码 | codex-app-server 仓库代码置于 `/app`(host.py 入口) | 版本见 §3,不挂载宿主代码目录(VM 与宿主无共享路径) |
|
||||
|
||||
@@ -52,7 +52,9 @@ chv-guest-<ubuntu 版本>-<codex 版本>-<仓库 commit 短 hash>
|
||||
- `vmlinux`(与 Ubuntu 云镜像配套的内核)
|
||||
- `disk.raw`(根文件系统镜像)
|
||||
- `BUILDINFO.json`:`{ubuntu, codex_version, repo_commit, built_at,
|
||||
contents: [清单逐项版本]}`
|
||||
contents: [清单逐项名]}`——注意:build.sh 生成的 contents 只有
|
||||
名字列表,没有逐项版本号(feat-chv-fix 纠正 §3 旧措辞);
|
||||
逐项版本只留在构建日志里,未进 BUILDINFO,这是已知缺口。
|
||||
|
||||
## 4. 构建脚本(补缺口轨:已从骨架改成真实步骤)
|
||||
|
||||
@@ -66,13 +68,13 @@ KVM;全量模式在无 /dev/kvm 的机器上第一步就明确退出
|
||||
本轨未在真机上跑过全量模式,镜像未建成、未发布,仍属
|
||||
“需 KVM 宿主实测”项。
|
||||
|
||||
## 6. Host key 首启注入硬性要求(补缺口轨新增)
|
||||
## 5. Host key 首启注入硬性要求(补缺口轨新增)
|
||||
|
||||
执行器的 per-VM 预置方案(见 `cloud/chv/hostkeys.py`)能
|
||||
跑通的前提是客机镜像必须满足以下全部,否则预置在真机
|
||||
上跑不通,执行器会 fail-fast,不回落成弱方案:
|
||||
- 装 cloud-init 且支持 NoCloud seed(seed 磁盘/目录由
|
||||
执行器在 VM 启动前生成,含 meta-data 与 user-data);
|
||||
- 装 cloud-init 且支持 NoCloud seed(seed 磁盘(CIDATA ISO,feat-chv-fix 起挂进 VmConfig
|
||||
第二块只读盘)由执行器在 VM 启动前生成,含 meta-data 与 user-data);
|
||||
- 首启时先应用 seed 再启动 sshd,seed 里的 user-data
|
||||
把该 VM 的 host key 对写进 /etc/ssh/ 并重启 sshd;
|
||||
- 镜像里已钉死 `ssh_deletekeys: false` 与
|
||||
@@ -80,16 +82,16 @@ KVM;全量模式在无 /dev/kvm 的机器上第一步就明确退出
|
||||
重新生成 host key——重生成会使预置的 known_hosts 立即
|
||||
失效,SSH 钉死直接拒绝,这是设计,不是 bug;
|
||||
- sshd 只允许 key 登录,禁止密码登录;
|
||||
- seed 磁盘/目录是 per-VM 敏感物,执行器在 VM 关闭/
|
||||
- seed 磁盘是 per-VM 敏感物,执行器在 VM 关闭/
|
||||
回收时删除,镜像构建方不得把任何一把 host key 固化
|
||||
进镜像(所有 VM 同 key 是安全事故)。
|
||||
|
||||
## 5. 与执行器的对接
|
||||
## 6. 与执行器的对接
|
||||
|
||||
- 执行器 preflight 只校验内核/镜像文件存在且可读,不校验镜像
|
||||
内容版本——内容版本核对(BUILDINFO)是镜像发布流程的责任,
|
||||
在就绪清单里标为“需镜像构建”项,不在本轨假装已做。
|
||||
- 每任务磁盘:执行器 copy 模式下把 `disk.raw` 复制为每任务
|
||||
磁盘,任务之间文件系统独立;reuse 模式仅单租户实验,生产禁用。
|
||||
磁盘,任务之间文件系统独立;reuse 模式仅单租户实验,preflight 拦截(须同时设 CHV_REUSE_SINGLE_TENANT_EXPERIMENT=1 并审计放行,feat-chv-fix)。
|
||||
- 快照基准:`CHV_SNAPSHOT_URL` 指向的快照必须由本镜像的干净
|
||||
启动状态制成(pause 后 snapshot),预热池用它做干净 VM 来源。
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
# 生产凭证;模型 key 由客机 cloud-init 预置的运行环境变量名
|
||||
# 注入,不写进镜像层。
|
||||
#
|
||||
# 客机 host key 硬性要求(见 README §6,补缺口轨新增):
|
||||
# 客机 host key 硬性要求(见 README §5,补缺口轨新增):
|
||||
# 客机必须装 cloud-init 且支持 NoCloud seed,首启先应用
|
||||
# seed 再启动 sshd,且不得无条件重新生成 host key,否则
|
||||
# 执行器的 per-VM 预置钉死即失效。本脚本在镜像里写
|
||||
|
||||
+52
-18
@@ -96,7 +96,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
wake_vm() 先 preflight,再经 launcher 以 --api-socket
|
||||
只启动 VMM 进程(CLI 上不带内核/磁盘配置),等
|
||||
vmm.ping 通,再 PUT vm.create(VmConfig)+ PUT vm.boot。
|
||||
build_vm_command() 保留的直接启动命令只是文档里的手动
|
||||
_build_vm_command() 保留的直接启动命令只是文档里的手动
|
||||
等价写法(运维手工起 VM 时用),执行器代码里不走它。
|
||||
- 就绪判定只选 SSH,不做 vsock 第二套:boot 后轮询 SSH
|
||||
“true”命令,直到 CHV_READY_TIMEOUT(默认 120 秒,
|
||||
@@ -207,7 +207,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
且可读、SSH key 存在且可读。任一项缺失都 raise
|
||||
RuntimeError,绝不静默回落 Docker/Local。_validate()
|
||||
即 preflight(旧名保留,供旧调用点用)。
|
||||
命令构造(build_vm_command/build_ssh_command/
|
||||
命令构造(_build_vm_command/build_ssh_command/
|
||||
build_vm_config)是纯函数,不做文件存在校验,单测可
|
||||
直接断言命令与 VmConfig 形状。
|
||||
|
||||
@@ -314,6 +314,35 @@ class CloudHypervisorExecutor(Executor):
|
||||
return os.path.isfile(path) and os.access(path, os.X_OK)
|
||||
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):
|
||||
"""预检(缺一不可则 fail-fast):收集全部缺失项一次报出
|
||||
具体缺哪一项,绝不笼统报错;失败为 ChvFailure(带 kind,
|
||||
@@ -411,17 +440,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
if not isinstance(_net, (list, dict)):
|
||||
missing.append(("net_config_invalid",
|
||||
"CHV_NET_JSON 须为 JSON list 或 dict"))
|
||||
# feat-chv-fix(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(仅单租户实验)"))
|
||||
self._preflight_config_checks(missing)
|
||||
if missing:
|
||||
kind = missing[0][0]
|
||||
detail = "; ".join(text for _, text in missing)
|
||||
@@ -451,8 +470,14 @@ class CloudHypervisorExecutor(Executor):
|
||||
pass
|
||||
|
||||
# -- 命令构造(纯函数,不做文件校验) -------------------------------
|
||||
def build_vm_command(self, task):
|
||||
"""构造 cloud-hypervisor 启动命令(干跑可测)。
|
||||
def _build_vm_command(self, task):
|
||||
"""构造 cloud-hypervisor 启动命令(干跑可测,仅私有)。
|
||||
|
||||
feat-chv-fix(P2-2):原公开 build_vm_command() 降级为
|
||||
私有——执行器唯一启动路径是 API 模式(wake_vm),公开
|
||||
的第二启动路径容易被后来人误当成可走的启动入口;手动
|
||||
等价写法保留在本方法与类 docstring 里,运维手工起 VM
|
||||
时仍可按此形状拼命令,不再经公开方法调用。
|
||||
|
||||
顺序固定:<二进制> --api-socket <socket> --cpus boot=N
|
||||
--memory size=M --kernel <内核> --cmdline <命令行>
|
||||
@@ -924,7 +949,7 @@ class CloudHypervisorExecutor(Executor):
|
||||
|
||||
def start_vm(self, task):
|
||||
"""旧名保留:即 wake_vm(API 模式)。直接启动命令
|
||||
build_vm_command() 只作手动等价写法,不再是执行器
|
||||
_build_vm_command() 只作手动等价写法(私有),不再是执行器
|
||||
的启动路径——两条启动路径并存是上一版的半吊子,
|
||||
本版只认 API 模式。"""
|
||||
return self.wake_vm(task)
|
||||
@@ -1102,8 +1127,17 @@ class CloudHypervisorExecutor(Executor):
|
||||
_p = os.path.join(self.state_dir, name, _suffix)
|
||||
if os.path.exists(_p):
|
||||
os.remove(_p)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as _cleanup_exc:
|
||||
# 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) \
|
||||
else {"id": task_or_name}
|
||||
if had:
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
seed 目录 state_dir/<vm>/seed/ 含 meta-data(instance-id
|
||||
即 VM 名)、user-data(write_files 写 /etc/ssh/ 的 host
|
||||
key 对并重启 sshd)。客机镜像硬性要求(见
|
||||
chv-guest/README.md §6):必须装 cloud-init 且支持
|
||||
chv-guest/README.md §5):必须装 cloud-init 且支持
|
||||
NoCloud seed、首启时先应用 seed 再启动 sshd、且不得在
|
||||
seed 应用前/后无条件重新生成 host key(否则预置即失效)。
|
||||
seed 磁盘/目录是 per-VM 敏感物,VM 关闭/回收时必须删。
|
||||
|
||||
@@ -1596,6 +1596,19 @@ class DockerExecutor(Executor):
|
||||
"container": self.container_name(task),
|
||||
"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)
|
||||
stdout_full = getattr(result, "stdout", "") or ""
|
||||
stderr = getattr(result, "stderr", "") or ""
|
||||
|
||||
@@ -15,6 +15,7 @@ import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import unittest
|
||||
from unittest import mock
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
@@ -550,5 +551,44 @@ class ExecuteDeadPortTest(ChvFixCase):
|
||||
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__":
|
||||
unittest.main()
|
||||
@@ -175,13 +175,40 @@ class BreakpointWiringTest(ChvGapCase):
|
||||
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))
|
||||
|
||||
def test_worker_branch_uses_hasattr_not_only_local(self):
|
||||
# worker.py 通用分支的文字证据:Local 优先、其次 hasattr。
|
||||
worker_path = Path(__file__).resolve().parents[1] / "worker.py"
|
||||
text = worker_path.read_text()
|
||||
self.assertIn("_call_goal_resume_breakpoint", text)
|
||||
self.assertIn("hasattr(", text)
|
||||
self.assertIn("goal_resume_breakpoint_task", text)
|
||||
def test_worker_breakpoint_branch_calls_generic_executor(self):
|
||||
# feat-chv-fix(P2-3):不再用源码 grep 充数,改为行为
|
||||
# 断言——真实 Worker._run_attempt 走通用断点分支,调到
|
||||
# 非 Local 执行器的 goal_resume_breakpoint_task,并把
|
||||
# 结果原样返回(参数、守卫顺序、返回值都覆盖)。
|
||||
from cloud.server import init_db
|
||||
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 接线 ---------------------------------------
|
||||
|
||||
@@ -264,7 +264,8 @@ class ConfigCommandTest(ChvCase):
|
||||
|
||||
def test_manual_vm_command_shape(self):
|
||||
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.assertIn("--api-socket", cmd)
|
||||
self.assertIn("--cpus", cmd)
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
宿主在启动前为每 VM 生成一次性 ed25519 host key 对,
|
||||
公钥立即写进该 VM 的 known_hosts 钉死,私钥只进 per-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)。
|
||||
ssh-keyscan 首次捕获只保留为弱回落(CHV_HOST_KEY_MODE
|
||||
=keyscan),生产部署必须用 provisioned 模式;TOFU 首次
|
||||
@@ -233,9 +233,17 @@
|
||||
CI 账单恢复。另:P0-2 审计原说法“restore 须把 seed 接入
|
||||
VmConfig”已纠正——restore 不走 vm.create,seed 无从挂盘,
|
||||
正确不变量是复用钉死 known_hosts。
|
||||
- ⛔ P2 全部排除、未做(用户紧急范围收窄令,留待以后一轨):
|
||||
长函数拆分、第二启动路径清理 / Docker goal 解析注释、
|
||||
test_chv_gaps.py 的 grep 文本断言改行为断言、build.sh 与
|
||||
README 口径对齐、stop_vm 吞异常改审计、配置校验时机统一。
|
||||
本轨的 grep 文本测试、README/build.sh、stop_vm 吞异常、
|
||||
preflight 校验口径均保持 #230 原样。
|
||||
- ✅🧪 P2 已做(范围已恢复为全做,上一条收窄令作废):
|
||||
test_chv_gaps.py 的 grep 文本断言改成真实 Worker._run_attempt
|
||||
行为断言;stop_vm 删除失败记 vm_key_cleanup_failed 审计
|
||||
(同名复用旧 key 的彻底根治“清理失败即拒起”列为遗留,不
|
||||
假装已根治);配置校验时机统一到 preflight(CPU/内存非法
|
||||
进 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 字段解析与
|
||||
其余长函数深耦合,本轨如实停、不硬拆)。
|
||||
Reference in new issue
Block a user