merge(chv-muse-round12): 合入 round-11 修复轮 HEAD 50cae2a (RETEST6 全绿)

按 hunk 合入,不整文件覆盖。冲突 3 处:
- chv-guest/build.sh:取 P4 的 copy-in(cdp-broker/guard/blocklist/
  chv-memory)+ cdp-broker service/enable 三件;取 R11 的 git
  config(去掉 credential.helper store,层 5);tmpfiles 两边
  并存(/run/chv/ask-queue + /run/chv-browser)。
- chv-guest/chv-browser.py:P4 架构保留(CDP broker 客户端 +
  guard 模块);取 R11 两处仍适用的运行期修复:BROWSER_SOCK
  移进 /run/chv-browser(缺陷 4,合入后的 broker 已指该路径)
  与 handle_request 认 op=browser/action 透传;R11 的直连
  Playwright 代码(validate_url/truncate_aria/_load_playwright/
  _page snapshot)由 P4 取代,不带回。
- cloud/tests/test_chv_muse_p2.py:truncate 三测取 P4 的
  GUARD.truncate_a11y(dict 树);FakeCdpBroker 取 P4 版,删
  R11 的 FakePage 系列;干跑夹具取 R11 的 _fixture 调用,并
  给 _fixture 补 P4 四件(否则合入后 build.sh fail-fast)。

合入内移植(R11 修复在 P4 里移了位置,不移植即静默丢失):
- chv-guest/chv-cdp-broker.py:_load_playwright 返回
  sync_playwright()(原 R11 在 chv-browser.py 修过的同形
  bug);回归测试由 test_p2 移植到 test_chv_muse_p4.py。
- test_chv_muse_p2.py:HOST 由模块级 eager 改懒加载(_host()):
  host.py 顶层 import queue 会把 stdlib queue 缓进 sys.modules,
  使同进程后加载的 test_queue_sandbox 取不到 cloud/queue.py
  遮蔽而 ImportError(全量唯一失败)。

未移植(见报告):R11 的 Page.aria_snapshot() 纠正未进
chv-cdp-broker.py(仍调 accessibility.snapshot(),且 guard
的 dict 树 API 须配套改,属 P4 返工,不在本轮合入做)。

全量 cloud 套件:2522 tests OK (skipped=10),基线 a6f9597
为 2474 OK (skipped=12);+48 来自 R11 新增测试(p2 +36、
p3 +7、closeup +3、gaps +1、p4 +1 移植)。
This commit is contained in:
host committed 2026-11-07 18:13:31 +00:00
commit 9da74c2a78
24 files changed
+1949 -217

No files matched your search

+31 -11
View File
@@ -250,8 +250,9 @@ class CloudHypervisorExecutor(Executor):
3-4294967295 内,0/1/2 为保留 CID)
- CHV_CELL_ENABLED:nspawn 单元开关(默认 1 开;只认
0/1,非法值 ValueError)。开时客内 host.py 改走
chv-cell run(P2 差距表 §4);关时走原命令,给
lite10 及更早无单元的旧镜像留逃生口
sudo chv-cell run(P2 差距表 §4;round-11 真机修复:
chv-cell 须 root,执行器以 chv 身份调,须 sudo);
关时走原命令,给 lite10 及更早无单元的旧镜像留逃生口
前置校验(preflight,执行入口必调,缺一即 fail-fast):
收集全部缺失项一次报出具体缺哪一项,绝不笼统报错:
@@ -1226,6 +1227,19 @@ class CloudHypervisorExecutor(Executor):
return normalize_api_result(
self.api_client(socket_path, method, path, payload))
@staticmethod
def _truncate_api_body(body, limit=500):
"""把 API 响应体压成单行短串(放进失败消息里定位用)。
vm.create/vm.boot 的错误体是 VMM 的报错文本,不含秘密;
超长即截断并标明,避免日志被巨体淹没。"""
if body is None:
return ""
text = body if isinstance(body, str) else repr(body)
text = " ".join(text.split())
if len(text) > limit:
return text[:limit] + "…(已截断)"
return text
def _call_runner(self, cmd, timeout=None):
"""runner 调用(共享 helper 在 cloud/sandbox.py):
timeout 给定时优先传 timeout,假 runner 只接受 cmd 时
@@ -1462,11 +1476,15 @@ class CloudHypervisorExecutor(Executor):
放在该路径(与 Docker 的 /app:ro 同款约定,只是载体
从容器换成 VM 镜像)。
round-11 集成补洞(P2 差距表 §4):单元开时(默认)
命令改为 chv-cell run -- python3 /app/host.py --state
命令改为 sudo chv-cell run -- python3 /app/host.py --state
/home/chv/.codex/host-state.json --codex-home
/home/chv/.codex … --sandbox danger-full-access [子命令
...]。--state/--codex-home 须显式给(见模块顶 _CELL_*
注释);--sandbox 放全局参数最后(argparse 后胜出),
...]。sudo 不可省:chv-cell 须 root(nspawn + 配网),
执行器经 SSH 以 chv 身份调,chv 的 NOPASSWD sudo 来自
seed users 块(见 test_chv_gitea_sync.py 的 users 夹具;
探针 p2.log 的手动调用也是 sudo chv-cell run)。
--state/--codex-home 须显式给(见模块顶 _CELL_* 注释);
--sandbox 放全局参数最后(argparse 后胜出),
确保单元内恒为 danger-full-access:单元即沙箱,调用
方传的 read-only(plan 任务)与 approval=full 自带的
值都不得把它盖回 bwrap。单元关时
@@ -1476,7 +1494,7 @@ class CloudHypervisorExecutor(Executor):
cell = self._cell_enabled()
cmd = ["python3", self.guest_host_py]
if cell:
cmd = ["chv-cell", "run", "--"] + cmd
cmd = ["sudo", "chv-cell", "run", "--"] + cmd
cmd += ["--state", _CELL_STATE_PATH,
"--codex-home", _CELL_CODEX_HOME]
cmd += _host_command_options(
@@ -1864,13 +1882,14 @@ class CloudHypervisorExecutor(Executor):
self._launch_api_only(task)
self.wait_api_ready(task)
self._check_api_socket_permissions(socket_path)
status, _ = self._api_call(
status, body = self._api_call(
socket_path, "PUT", "/vm.create",
self.build_vm_config(task_disk, seed_disk=seed_disk))
if status not in (200, 204):
raise ChvFailure(
"vm_create_failed",
"vm.create 失败:status=%s" % status)
"vm.create 失败:status=%s body=%s"
% (status, self._truncate_api_body(body)))
self._emit("vm_create", task,
"vm=%s disk=%s" % (name, task_disk))
if net_entries:
@@ -1881,11 +1900,12 @@ class CloudHypervisorExecutor(Executor):
"net_registry_write_failed",
"vm=%s net 登记写失败,拒绝启动(缺登记会"
"漏判 tap/mac/ip 冲突)" % name)
status, _ = self._api_call(socket_path, "PUT", "/vm.boot")
status, body = self._api_call(socket_path, "PUT", "/vm.boot")
if status not in (200, 204):
raise ChvFailure(
"vm_boot_failed",
"vm.boot 失败:status=%s" % status)
"vm.boot 失败:status=%s body=%s"
% (status, self._truncate_api_body(body)))
self._emit("vm_boot", task, "vm=%s" % name)
self.wait_guest_ready(task)
# V-9 移植:密钥已投递时等 guest-env 对 SSH 用户可读再报
@@ -2622,7 +2642,7 @@ class CloudHypervisorExecutor(Executor):
ValueError(_to_switch 的 CHV_BALLOON/CHV_VSOCK 同款
口径,绝不静默 fallback)。关时 _guest_host_command
走原命令,给 lite10 及更早无单元的旧镜像留逃生口;
开时走 chv-cell run -- python3 /app/host.py …。读
开时走 sudo chv-cell run -- python3 /app/host.py …。读
self._env(与 _resident_enabled 同款惰性读法),不
经 config:开关只影响命令构造,不影响建池/起 VM。"""
flag = _to_switch(
+18 -6
View File
@@ -193,10 +193,16 @@ def build_seed_user_data(private_key_text: str, public_key_text: str) -> str:
def _authd_env_entry(authd_env: dict) -> list:
"""authd-seed 的 write_files 条目行(0600 root,值经
shlex.quote 引用):只装 AUTHD_SECRET_KEYS 命中的密钥,
与 guest-env 条目并列写进同一 write_files 列表(绝不另写
一个 write_files: 键,见 _guest_env_entry 的 YAML 重复键
禁令)。"""
shlex.quote 引用):装 AUTHD_SECRET_KEYS 命中的密钥
(本期只有 GITEA_TOKEN),外加与 guest-env 同源抄进来的
三个非密钥同步变量(GITEA_URL/GITEA_USER/SYNC_REPO——
round-11 真机修复层 2:authd 的 build_server 要求 seed
里同时有这三键,只 GITEA_TOKEN 会 ValueError;探针
fixprobe3 即从 guest-env 补这三键)。三键只在调用方
guest-env 里有才抄,没有不补、不编默认值;与 guest-env
条目并列写进同一 write_files 列表(绝不另写一个
write_files: 键,见 _guest_env_entry 的 YAML 重复键禁令)。
"""
lines = [
" - path: %s" % AUTHD_SEED_PATH,
" permissions: '0600'",
@@ -296,12 +302,18 @@ def _write_seed_meta(seed_dir: str, vm_name: str) -> str:
def _seed_env_entries(merged_env: dict,
authd_env: dict | None) -> list:
"""guest-env 与 authd-seed 两路的 write_files 条目行
(皆空即空列表,不写条目)。"""
(皆空即空列表,不写条目)。authd-seed 除了密钥还抄
guest-env 里的 GITEA_URL/GITEA_USER/SYNC_REPO(层 2,
见 _authd_env_entry)——build_server 要求四键齐全。"""
extra_lines = []
if merged_env:
extra_lines += _guest_env_entry(merged_env)
if authd_env:
extra_lines += _authd_env_entry(authd_env)
seed_env = dict(authd_env)
for key in ("GITEA_URL", "GITEA_USER", "SYNC_REPO"):
if key in merged_env:
seed_env[key] = merged_env[key]
extra_lines += _authd_env_entry(seed_env)
return extra_lines
+4 -1
View File
@@ -93,7 +93,9 @@ _BLOCKED_NETWORKS = tuple(
# 每连接器的 L7 路径策略:(方法, 匹配方式, 模式)。
# gitea:API 只放 /api/v1/;git 智能 HTTP 只放三条(clone/pull/push
# 的实际路径,见 git http-protocol)。其它路径(Web UI、登录页、
# 的实际路径,见 git http-protocol)加裸 .git(round-11 真机修复
# 层 4:git 智能 HTTP 的发现请求只发裸 .git 路径,裸路径只放 GET、
# 一律当 pull;POST 裸 .git 仍拒绝)。其它路径(Web UI、登录页、
# 任意管理接口)一律不在本连接器的能力内。
# feat-chv-muse-p9 的 inference 条目:推理连接器只放
# POST /v1/responses 与 GET /v1/models(/v1/models 是否必需待
@@ -104,6 +106,7 @@ _CONNECTOR_PATH_RULES = {
("GET", "prefix", "/api/v1/"),
("POST", "prefix", "/api/v1/"),
("GET", "suffix", ".git/info/refs"),
("GET", "suffix", ".git"),
("POST", "suffix", ".git/git-upload-pack"),
("POST", "suffix", ".git/git-receive-pack"),
),