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

+55 -2
View File
@@ -51,6 +51,7 @@ import os
import queue
import re
import shlex
import shutil
import subprocess
import sys
import threading
@@ -62,6 +63,40 @@ from urllib.parse import urlparse
HERE = os.path.dirname(os.path.abspath(__file__))
DEFAULT_STATE = os.path.join(HERE, "host-state.json")
# part 6(RETEST5 新发现):单元内 worktree 根,绝不在 /app 之下。
# 单元内 HERE=/app,而 /app 是 chv-cell 的 --bind-ro;part 5 曾
# 把客机 workspace 下的 .chv-worktrees 以可写 bind 挂到
# /app/work 上(argv 里排在 --bind-ro=/app 之后,指望后挂盖住
# 只读子树),RETEST5 真机证明该设想不成立:后挂的 bind 只有在
# 挂载点已存在时才能盖住,挂载点不存在时 nspawn 在只读 /app 下
# 连建都建不起来(`Failed to create mount point .../app/work:
# Read-only file system`,全量 73 次,CELL-RC=0 共 0 次),
# 所有 cell 启动失败。修法三选一取 (a) worktree 根移出 /app:
# 单元内改用 workspace 绑定下已可写的
# /home/chv/workspace/.chv-worktrees(该路径经 --bind 整个
# workspace 以可写方式进单元,无需任何额外 bind,挂载点问题
# 按构造即不存在);(b) 默认 --no-worktree 不取:隔离丢了,
# 且 goal/fork 同样调 make_worktree,照样撞 Errno 30;(c) 镜像
# 里预建空目录 /app/work 不取:修法耦合镜像构建,某次重建漏
# 建即静默回归,且单测无法钉住镜像内容。
# CHV_WORKTREE_ROOT 由 chv-cell 经 --setenv 传进来(与
# chv-cell.py 的 WORKTREE_DIR 同值);CHV_CELL=1 但变量未设
# 时(旧版 chv-cell / 单元内手动跑 host.py)回落到同一单元内
# 路径,单元内 worktree 根永不在 /app 之下;单元外沿用
# HERE/work/host-worktrees,行为不变。
CHV_WORKTREE_ROOT_ENV = "CHV_WORKTREE_ROOT"
CELL_WORKTREE_ROOT = "/home/chv/workspace/.chv-worktrees"
def worktree_base():
"""worktree 根目录(<根>/host-worktrees/<任务>)。"""
root = os.environ.get(CHV_WORKTREE_ROOT_ENV)
if root:
return os.path.join(root, "host-worktrees")
if os.environ.get("CHV_CELL") == "1":
return os.path.join(CELL_WORKTREE_ROOT, "host-worktrees")
return os.path.join(HERE, "work", "host-worktrees")
def _sum_turn_usage(events, thread_id, turn_id):
"""从事件窗口里汇总一回合的上游真实用量。
@@ -203,6 +238,22 @@ def default_codex_bin():
paired = os.path.join(HERE, "bin", "codex")
if os.path.exists(paired):
return paired
# part 5(RETEST4 新发现 1):单元内既无 CODEX_BIN 又无
# HERE/bin/codex 时,旧代码返回裸 "codex",run_health_check
# 的 os.path.exists("codex") 以相对路径判恒为 False,
# health 的 ok 恒 False(单元内 which codex 明明是
# /usr/local/bin/codex)。回落到 shutil.which("codex"):
# PATH 里找得到即返回绝对路径,health 与 Server 都直接
# 受益;找不到时仍返回裸 "codex",行为与旧代码一致。
# 不选 (a) CODEX_BIN 进 FORWARD_ENV_ALLOWLIST:要执行器
# 与 chv-cell 两边接线、还得在客机环境里定一个单元内路径,
# 改动面大,且只修执行器驱动的调用;不选 (b) 执行器显式
# 加 --codex-bin:把客机镜像路径硬编码进执行器,且直接
# 跑 host.py 的 health 同样修不到。单点修 host.py 改动
# 最小、覆盖全部调用方。
found = shutil.which("codex")
if found:
return found
return "codex"
@@ -1310,7 +1361,7 @@ def git(args, cwd):
def make_worktree(repo, task_name):
"""一任务一 worktree;不是 git 仓库时退化为独立目录拷贝。"""
repo = os.path.abspath(repo)
base = os.path.join(HERE, "work", "host-worktrees")
base = worktree_base()
os.makedirs(base, exist_ok=True)
path = os.path.join(base, task_name)
if os.path.exists(path):
@@ -1842,7 +1893,9 @@ def dispatch_from_branch(server, state, name, prompt, repo, dep_branch,
c = git(["commit", "-m", f"host: {name} base from {dep_branch}"],
dep_worktree)
# nothing to commit 时分支里已有全部改动,继续用它当基底
base = os.path.join(HERE, "work", "host-worktrees")
# part 6:与 make_worktree 同款,worktree 根经 worktree_base()
# 选(单元内在 workspace 下,不在只读 /app 之下)。
base = worktree_base()
os.makedirs(base, exist_ok=True)
path = os.path.join(base, name)
if os.path.exists(path):