From 2ad29eeb1fcc27faef5db893e63d39d8e7099761 Mon Sep 17 00:00:00 2001 From: host Date: Fri, 6 Nov 2026 18:41:56 +0000 Subject: [PATCH] =?UTF-8?q?feat(chv-guest):=20M4=20Chromium=20=E5=8D=95?= =?UTF-8?q?=E7=8B=AC=E9=92=89=E6=AD=BB=20+=20/app=20=E6=94=B6=E7=B4=A7?= =?UTF-8?q?=E4=B8=BA=E6=9C=80=E5=B0=8F=E9=9B=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Chromium 钉死:CHROMIUM_HEADLESS_SHELL_REV(纯数字修订号) 设即钉死,buildinfo.py 校验读回的 ms-playwright 缓存目录名 须恰好是该修订号,否则构建失败;generate/check 的 --verify 也复核;未设时只记录、构建输出明说未钉死,BUILDINFO 记 null - /app 最小集:build.sh 不再把整个 $REPO_DIR/cloud/ 拷进客机, 只拷 host.py(只用标准库,不依赖 cloud/);REPO_DIR 预检 不再要求 cloud/ 存在 - generate_lockfile.py 只读 BUILDINFO.versions 一事已在 to10 轨 改写,本棒以合成 BUILDINFO 干跑复核(假版本 9.9.9 进 lockfile, 宿主 git 1:2.43.0 未混入)并补钉死复核;真实 lockfile 产出 仍待 V-9 真机构建,不编造版本号 --- chv-guest/README.md | 13 ++++++++--- chv-guest/build.sh | 29 +++++++++++++++++++----- chv-guest/buildinfo.py | 40 +++++++++++++++++++++++++++++----- chv-guest/check_lockfile.py | 23 ++++++++++++++----- chv-guest/generate_lockfile.py | 15 +++++++++++++ 5 files changed, 99 insertions(+), 21 deletions(-) diff --git a/chv-guest/README.md b/chv-guest/README.md index 5646bdd..8349239 100644 --- a/chv-guest/README.md +++ b/chv-guest/README.md @@ -30,10 +30,10 @@ | 语言与工具链 | Python 3.12、git、curl、ca-certificates、xz-utils、fontconfig、openssh-client(Node.js 不装:Playwright 驱动是 Python playwright,见 PLAYWRIGHT=1 分支;任务镜像 Dockerfile 里也没有 Node.js,本清单不虚构它) | 构建时钉小版本并记入构建日志;apt 用 --no-install-recommends(B-21:默认装 Recommends 会连带拉入 gcc 全套工具链顶满根分区) | | 文档/图片工具 | 已裁剪:pandoc、poppler-utils、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-headless-shell(默认装,不要完整 Chromium) | Python playwright 装当时的 headless-shell,版本以 ms-playwright 缓存目录名记入 BUILDINFO.versions.chromium;浏览器在系统路径 /usr/local/share/ms-playwright(B-22:virt-customize 里 HOME=/,默认缓存会落到根目录 /.cache),agent 运行时须设 PLAYWRIGHT_BROWSERS_PATH 指向它 | +| 浏览器 | Playwright + chromium-headless-shell(默认装,不要完整 Chromium) | Python playwright 装当时的 headless-shell,版本以 ms-playwright 缓存目录名记入 BUILDINFO.versions.chromium;浏览器在系统路径 /usr/local/share/ms-playwright(B-22:virt-customize 里 HOME=/,默认缓存会落到根目录 /.cache),agent 运行时须设 PLAYWRIGHT_BROWSERS_PATH 指向它;单独钉死见 §3:CHROMIUM_HEADLESS_SHELL_REV 设为纯数字修订号即钉死,未设时只记录、不强制 | | 客机基础 | openssh-server(SSH 通道用)、cloud-init(qemu-guest-agent 已裁剪,CHV 不用它) | sshd 只允许 key 登录,禁止密码登录——build.sh 写入 /etc/ssh/sshd_config.d/99-chv.conf 并经 sshd -t + grep 断言(feat-chv-secfix,M8,此前只是文档声称) | | Gitea 同步 | gt 1.0.0(官方 install.sh + SHA256 校验)、chv-sync(启动时 clone、显式 pull/push 的同步脚本) | 凭证走 seed 盘 /etc/chv/guest-env(0600)投递,不进镜像;见 §7 | -| 任务代码 | codex-app-server 仓库代码置于 `/app`(host.py 入口) | 版本见 §3,不挂载宿主代码目录(VM 与宿主无共享路径) | +| 任务代码 | 仅 `/app/host.py` 一个文件(host.py 只用标准库,不依赖 cloud/;旧版把整个 cloud/ 拷进 /app/,客内可读全套 server/worker 代码,M4 收尾已收紧为最小集) | 版本见 §3(repo_commit),不挂载宿主代码目录(VM 与宿主无共享路径) | 中文 LaTeX 说明:与 Docker 镜像同款口径——xelatex + CJK 字体会再增数百 MB,本清单不默认包含;需要时单独一版镜像, @@ -60,13 +60,20 @@ chv-guest---<仓库 commit 短 hash> 在 `versions` 里(B-02:dpkg + pip freeze 从客机读回, chromium 以 ms-playwright 缓存目录名记,gt 以 gt_version 记), `generate_lockfile.py` / `check_lockfile.py` 以它为唯一来源。 + Chromium 单独钉死:构建时设 CHROMIUM_HEADLESS_SHELL_REV + 为纯数字修订号(如 1248),buildinfo.py 校验读回的缓存 + 目录名须恰好是 `chromium_headless_shell-`,否则构建 + 失败,BUILDINFO.chromium_pinned_rev 记该值;未设时记 null、 + 只记录不强制,构建输出明说“未钉死”。真实 lockfile 产出 + 须等 V-9 真机构建(本机无 /dev/kvm,版本号绝不编造)。 ## 4. 构建脚本(补缺口轨:已从骨架改成真实步骤) 见 `build.sh`:参数校验 → 下载 Ubuntu Minimal 云镜像(SHA256 必填)→ qemu-img 转 raw(轻量版不扩盘,底座根分区 2.4G 够用; 全量版 B-16 的 +12G 扩盘已删除)→ virt-customize 装 §2 清单、 -拷入 codex 成套二进制与仓库代码到 /app、装 gt 1.0.0、拷入 +拷入 codex 成套二进制与 host.py(仅此一个文件,不拷 cloud/) +到 /app、装 gt 1.0.0、拷入 chv-sync 并写 cloud-init 启动钩子、写 host key 不重生成配置 → virt-get-kernel 取内核 → 生成并校验 BUILDINFO.json。 `./build.sh --dry-run out/` 只打印命令、不执行、不要求 diff --git a/chv-guest/build.sh b/chv-guest/build.sh index aa01c31..2896c87 100755 --- a/chv-guest/build.sh +++ b/chv-guest/build.sh @@ -7,7 +7,10 @@ # ubuntu-24.04-minimal-cloudimg-amd64.img——minimal 发布在 noble # 代号目录下,不是 releases/24.04)。只装 codex 0.164.0-alpha.9 # linux-x64 + Python + SSH + cloud-init + 一个薄客机 agent -# (/app/host.py + /app/cloud)。浏览器:Playwright + +# (/app/host.py,仅此一个文件——host.py 只用标准库,不依赖 +# cloud/;旧版把整个 $REPO_DIR/cloud/ 拷进 /app/,客内 agent +# 可读全套 server/worker 代码,M4 收尾已收紧为最小集)。 +# 浏览器:Playwright + # chromium-headless-shell(不要完整 Chromium);字体只要最小的 # fonts-wqy-microhei(中文能看),不要全套 fonts-noto-cjk。 # 目标:qcow2 实际占用 ≤1GB,raw ≤2GB(见下注)。 @@ -84,6 +87,18 @@ REPO_DIR="${REPO_DIR:-$(cd "$SCRIPT_DIR/.." && pwd)}" # 验 SHA256;此处再验脚本自身的 SHA256(2026-11-06 实测值, # 上游轮换脚本后构建会明确失败,不静默跑新脚本)。 GT_VERSION="${GT_VERSION:-1.0.0}" +# feat-chv-closeup(M4):Chromium 单独钉死。PLAYWRIGHT=1 时 +# ms-playwright 缓存目录名即 Chromium 版本标识(如 +# chromium_headless_shell-1248),此前只记录不钉死——playwright +# 一升级,目录名跟着变,构建静默换浏览器版本。 +# CHROMIUM_HEADLESS_SHELL_REV 设为纯数字修订号(如 1248)即钉死: +# buildinfo.py 校验读回的目录名须恰好是该修订号,否则构建失败。 +# 未设时只记录、不强制,并在构建输出里明说“未钉死”,绝不假装 +# 已钉死;真机 V-9 验收须用已设该变量的构建。 +CHROMIUM_HEADLESS_SHELL_REV="${CHROMIUM_HEADLESS_SHELL_REV:-}" +if [ -n "$CHROMIUM_HEADLESS_SHELL_REV" ] && ! printf '%s' "$CHROMIUM_HEADLESS_SHELL_REV" | grep -Eq '^[0-9]+$'; then + echo "CHROMIUM_HEADLESS_SHELL_REV 须为纯数字修订号(如 1248):$CHROMIUM_HEADLESS_SHELL_REV" >&2; exit 1 +fi GT_INSTALL_SH_SHA256="${GT_INSTALL_SH_SHA256:-cf82acecc0e3ffa6f690d33902eefcf1cc21a5b11004103bf23c4436664288d5}" # B-03/B-04:输入格式校验(干跑与全量都做,报错点提前, # 不等到 curl 404 或 sha256sum -c 才失败)。 @@ -143,8 +158,8 @@ if [ ! -d "$CODEX_BIN_DIR/codex-resources" ]; then echo "缺少 codex-resources(bwrap 载体),构建失败" >&2 exit 1 fi -if [ ! -f "$REPO_DIR/host.py" ] || [ ! -d "$REPO_DIR/cloud" ]; then - echo "REPO_DIR 不是 codex-app-server 仓库根(缺 host.py/cloud/):$REPO_DIR" >&2 +if [ ! -f "$REPO_DIR/host.py" ]; then + echo "REPO_DIR 不是 codex-app-server 仓库根(缺 host.py):$REPO_DIR" >&2 exit 1 fi if [ ! -f "$SCRIPT_DIR/chv-sync.sh" ]; then @@ -241,7 +256,6 @@ VC_ARGS=(-a "$OUT/disk.raw" --run-command 'chmod +x /usr/local/bin/codex /usr/local/bin/codex-code-mode-host; ln -sf /usr/local/bin/codex-resources/bwrap /usr/local/bin/bwrap' --mkdir /app --copy-in "$REPO_DIR/host.py:/app/" - --copy-in "$REPO_DIR/cloud:/app/" --copy-in "$SCRIPT_DIR/chv-sync.sh:/usr/local/bin/" --run-command 'mv /usr/local/bin/chv-sync.sh /usr/local/bin/chv-sync && chmod +x /usr/local/bin/chv-sync' --run-command 'printf "runcmd:\n - systemctl restart ssh || service ssh restart || true\n - /usr/local/bin/chv-sync boot >>/var/log/chv-sync.log 2>&1 || true\n" > /etc/cloud/cloud.cfg.d/99-chv-sync.cfg' @@ -302,11 +316,14 @@ fi # 产物校验(干跑只打印,不执行校验命令的副作用部分)。 if [ "$DRY_RUN" = "1" ]; then echo "DRY-RUN: test -s $OUT/vmlinux && test -s $OUT/disk.raw" - echo "DRY-RUN: python3 $SCRIPT_DIR/buildinfo.py $OUT $TAG $UBUNTU_VERSION $CODEX_VERSION $REPO_COMMIT ${PLAYWRIGHT:-1} # 生成 $OUT/BUILDINFO.json 并校验 tag=$TAG" + echo "DRY-RUN: python3 $SCRIPT_DIR/buildinfo.py $OUT $TAG $UBUNTU_VERSION $CODEX_VERSION $REPO_COMMIT ${PLAYWRIGHT:-1} ${CHROMIUM_HEADLESS_SHELL_REV:-} # 生成 $OUT/BUILDINFO.json 并校验 tag=$TAG" else test -s "$OUT/vmlinux" || { echo "内核产物为空:$OUT/vmlinux" >&2; exit 1; } test -s "$OUT/disk.raw" || { echo "磁盘产物为空:$OUT/disk.raw" >&2; exit 1; } - python3 "$SCRIPT_DIR/buildinfo.py" "$OUT" "$TAG" "$UBUNTU_VERSION" "$CODEX_VERSION" "$REPO_COMMIT" "$UBUNTU_IMAGE_SHA256" "${PLAYWRIGHT:-1}" + if [ "${PLAYWRIGHT:-1}" = "1" ] && [ -z "$CHROMIUM_HEADLESS_SHELL_REV" ]; then + echo "警告:CHROMIUM_HEADLESS_SHELL_REV 未设,Chromium 版本只记录、不钉死(V-9 验收须设该变量重建)" >&2 + fi + python3 "$SCRIPT_DIR/buildinfo.py" "$OUT" "$TAG" "$UBUNTU_VERSION" "$CODEX_VERSION" "$REPO_COMMIT" "$UBUNTU_IMAGE_SHA256" "${PLAYWRIGHT:-1}" "${CHROMIUM_HEADLESS_SHELL_REV:-}" fi echo "tag=$TAG" echo "out=$OUT" diff --git a/chv-guest/buildinfo.py b/chv-guest/buildinfo.py index a093fd6..112b16c 100644 --- a/chv-guest/buildinfo.py +++ b/chv-guest/buildinfo.py @@ -11,6 +11,14 @@ sha256/size 是对产物文件实测的;image_sha256 即 disk.raw 用法: python3 buildinfo.py OUT_DIR TAG UBUNTU CODEX_VERSION \\ REPO_COMMIT UBUNTU_IMAGE_SHA256 PLAYWRIGHT(0|1) + [CHROMIUM_HEADLESS_SHELL_REV] + +CHROMIUM_HEADLESS_SHELL_REV(可选,纯数字):钉死的 +ms-playwright 缓存修订号(如 1248)。PLAYWRIGHT=1 且 +给了该值时,versions.chromium 里须恰好出现 +chromium_headless_shell-,否则退出 1、构建失败; +未给时只记录、不强制,BUILDINFO.chromium_pinned_rev +记 null,构建输出由 build.sh 明说“未钉死”。 """ import hashlib import json @@ -59,7 +67,7 @@ def _sha256_size(path): def build(outdir, tag, ubuntu, codex_version, repo_commit, - ubuntu_image_sha256, playwright): + ubuntu_image_sha256, playwright, chromium_pinned_rev=None): disk_path = os.path.join(outdir, "disk.raw") vmlinux_path = os.path.join(outdir, "vmlinux") disk_sha, disk_size = _sha256_size(disk_path) @@ -98,6 +106,9 @@ def build(outdir, tag, ubuntu, codex_version, repo_commit, "playwright": bool(playwright), "playwright_browsers_path": PLAYWRIGHT_BROWSERS_PATH if playwright else "", + # feat-chv-closeup(M4):钉死的 Chromium 修订号(未设 + # 记 null,绝不编一个默认值出来假装已钉死)。 + "chromium_pinned_rev": chromium_pinned_rev or None, "image_sha256": disk_sha, "artifacts": { "disk.raw": {"sha256": disk_sha, "size": disk_size}, @@ -112,15 +123,22 @@ def build(outdir, tag, ubuntu, codex_version, repo_commit, def main(argv): - if len(argv) != 8: + if len(argv) not in (8, 9): print("用法:buildinfo.py OUT TAG UBUNTU CODEX_VERSION " - "REPO_COMMIT UBUNTU_IMAGE_SHA256 PLAYWRIGHT", - file=sys.stderr) + "REPO_COMMIT UBUNTU_IMAGE_SHA256 PLAYWRIGHT " + "[CHROMIUM_HEADLESS_SHELL_REV]", file=sys.stderr) return 2 _, outdir, tag, ubuntu, codex_version, repo_commit, \ - ubuntu_image_sha256, playwright = argv + ubuntu_image_sha256, playwright = argv[:8] + chromium_pinned_rev = argv[8] if len(argv) == 9 else None + if chromium_pinned_rev and not re.fullmatch(r"[0-9]+", + chromium_pinned_rev): + print("CHROMIUM_HEADLESS_SHELL_REV 须为纯数字:%s" + % chromium_pinned_rev, file=sys.stderr) + return 2 info = build(outdir, tag, ubuntu, codex_version, repo_commit, - ubuntu_image_sha256, playwright == "1") + ubuntu_image_sha256, playwright == "1", + chromium_pinned_rev) if not info["versions"]["dpkg"]: print("BUILDINFO versions.dpkg 为空", file=sys.stderr) return 1 @@ -134,6 +152,16 @@ def main(argv): print("BUILDINFO versions.chromium 为空(浏览器未装好或" "读回路径不对)", file=sys.stderr) return 1 + if playwright == "1" and chromium_pinned_rev: + # feat-chv-closeup(M4):钉死校验——读回的缓存目录名 + # 须恰好是该修订号,playwright 升级导致目录名变了就 + # 在这里失败,不静默换浏览器版本。 + expected = "chromium_headless_shell-%s" % chromium_pinned_rev + if expected not in (info["versions"].get("chromium") or []): + print("Chromium 修订号与钉死值不符:读回 %s,钉死 %s" + % (info["versions"].get("chromium"), expected), + file=sys.stderr) + return 1 if info["tag"] != tag: print("BUILDINFO tag 不一致", file=sys.stderr) return 1 diff --git a/chv-guest/check_lockfile.py b/chv-guest/check_lockfile.py index 7e72e60..53492ef 100644 --- a/chv-guest/check_lockfile.py +++ b/chv-guest/check_lockfile.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 """校验已构建镜像的 BUILDINFO 供应链清单(事后 lockfile 检查)。 -事前 apt/pip 精确 lockfile 无法在本轨生成:apt 版本在构建 -宿主上解析、pip 版本在客机里解析,没有构建宿主就编不出 -版本号——本脚本绝不编造。构建时 build.sh 已从客机读回 -dpkg + pip freeze 写进 BUILDINFO.versions(事后 lockfile), -本脚本校验它:versions 非空、每条有版本、contents 全覆盖、 -tag 与预期一致、可选的冻结清单(--frozen JSON,形如 +生成端见 generate_lockfile.py(读 BUILDINFO.versions 生成 +lockfile.json,版本唯一来源是 build.sh 从客机读回的 +dpkg/pip freeze + chromium 目录名 + codex/repo_commit/gt +声明字段,绝不查构建宿主现场、绝不编造版本号)。 +本脚本校验 BUILDINFO:versions 非空、每条有版本、contents +全覆盖、tag 与预期一致、钉死的 Chromium 修订号一致、 +可选的冻结清单(--frozen JSON,形如 {"dpkg": ["pkg version", ...]})双向比对,漂移即退出非 0。 用法: @@ -111,6 +112,16 @@ def main(argv): if missing: print("contents 未覆盖:%s" % sorted(missing), file=sys.stderr) return 1 + # feat-chv-closeup(M4):钉死的 Chromium 修订号须与读回 + # 的缓存目录名一致;未钉(pinned_rev 为空)时不校验。 + pinned = data.get("chromium_pinned_rev") + if pinned: + expected = "chromium_headless_shell-%s" % pinned + if expected not in (versions.get("chromium") or []): + print("Chromium 修订号与钉死值不符:读回 %s,钉死 %s" + % (versions.get("chromium"), expected), + file=sys.stderr) + return 1 if frozen_path: with open(frozen_path) as fh: frozen = json.load(fh) diff --git a/chv-guest/generate_lockfile.py b/chv-guest/generate_lockfile.py index 5fa0ca9..cfe3c77 100644 --- a/chv-guest/generate_lockfile.py +++ b/chv-guest/generate_lockfile.py @@ -145,6 +145,21 @@ def verify(lockfile_path, buildinfo_path): % (lockfile.get("tag"), buildinfo.get("tag")), file=sys.stderr) return 1 + # feat-chv-closeup(M4):钉死的 Chromium 修订号须与 + # lockfile 里记的 chromium-headless-shell 条目一致; + # BUILDINFO 没钉(pinned_rev 为空)时不校验。 + pinned = buildinfo.get("chromium_pinned_rev") + if pinned: + expected = "chromium_headless_shell-%s" % pinned + declared = (lockfile.get("versions") or {}).get( + "declared") or [] + hit = [e for e in declared + if str(e).startswith("chromium-headless-shell ")] + if not hit or expected not in str(hit[0]): + print("Chromium 修订号与钉死值不符:lockfile %s," + "钉死 chromium_headless_shell-%s" + % (hit, pinned), file=sys.stderr) + return 1 versions = lockfile.get("versions") or {} for kind in ("dpkg", "pip"): entries = versions.get(kind)