Files
codex-app-server/chv-guest/buildinfo.py
T
host 1c8fc3f7c8 对齐 Muse P4:浏览器完全对齐(CDP broker 单元外 + a11y-tree-only + 确定性分类器)
- chv-guest/chv-cdp-broker.py(新):单元外 CDP broker,用户 chv-browser,
  独家 chromium.launch(headless=True, args=LAUNCH_ARGS);启动参数白名单
  无 --remote-debugging-port、无 devtools;导航前 URL 校验 + 拦截名单;
  快照恒带 untrusted_input 与 injection_flags;高风险表单在页时 open
  即 ask 入队;观测到导航 POST 自动暂停;命中拦截规则的下载取消 + 暂停
  + ask;全部回包与错误经 sanitize_error + response_leaks 检查。
- chv-guest/chv-browser.py(P2 版重构):改为 broker 客户端,不再直连
  Playwright(本文件无 playwright import),保留 URL 校验 + 快照截断
  作为第二层防线;暂停状态与 broker 同步。
- chv-guest/chv_browser_guard.py(新):纯 stdlib 共享规则——validate_url、
  拦截名单加载/匹配、快照截断、错误消毒(去 HTML、截断 200 字符、凭据形
  key=value 与 sk- 形 token 打码)、注入启发式(中英文各 6 条)、表单风险
  扫描、下载扫描(可执行扩展名)、文件型 ask 队列(pending/allowed/denied,
  与 P3 ApprovalQueue 同语义)、启动参数与回包泄露检查、脚本注入面
  禁止清单(FORBIDDEN_PAGE_ATTRS/FORBIDDEN_OPS)。
- chv-guest/browser-blocklist.txt(新):静态拦截名单;首版只有 RFC 2606
  .test 自测条目,生产条目由运维构建时填入,无在线更新。
- chv-guest/build.sh/buildinfo.py:P2 三脚本 + P4 三文件纳入镜像与
  buildinfo;新增 chv-cdp-broker.service 与 /run/chv/ask-queue tmpfiles。
- docs/chv-muse-p4-gap.md(新):现状 vs Muse 官方要求差距表、改动、
  诚实边界、P2 合入约定、真机待验证清单(round 12)。
- docs/chv-production-readiness.md:补 §15(P4 一节;编号合入时若与
  P2 的 §15 撞,顺延)。
- cloud/tests/test_chv_muse_p4.py(新,36 项):guard 单元、AST 钉住
  禁止属性、假 Playwright broker 测试、假 broker agent 测试、真 Unix
  socket 联调、buildinfo/build 干跑断言;含否定测试(evaluate 被拒、
  DOM 外泄被拒、DevTools 不可达、私有地址被拒、高风险表单被拦、
  注入文本被标注)。

测试:P4 新测试 36/36;全量 cloud 套件 1903 项,两次连续全过
(skipped=8)。附记:第一次全量跑出 1 个失败但输出未保留、后两次
均未复现,疑为时序抖动,未追认。
真机验证未做:排队等 round-11/第三棒释放 100.65.51.33,清单见 gap
表 §5。本地提交,未推未合未部署。
2026-11-06 22:36:56 +00:00

188 lines
7.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""生成 CHV 客机镜像的 BUILDINFO.json(build.sh 调用)。
从 build.sh 抽出(B-02),使其可独立单元测试。输入全部是
已落盘的构建产物与读回文件,不编造版本号:versions 来自
客机读回的 dpkg-query / pip freeze(Playwright=1 时另有
ms-playwright 缓存目录名作 chromium 标识);artifacts 的
sha256/size 是对产物文件实测的;image_sha256 即 disk.raw
的 sha256,供 generate_lockfile.py 沿用。
用法:
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-<rev>,否则退出 1、构建失败;
未给时只记录、不强制,BUILDINFO.chromium_pinned_rev
记 null,构建输出由 build.sh 明说“未钉死”。
"""
import hashlib
import json
import os
import re
import sys
import time
# 轻量口径(2026-11-06 用户定):Ubuntu Minimal 底座,只装
# codex + Python + SSH + cloud-init + 薄 agent + git + gt +
# 最小中文字体;已裁剪 pandoc/texlive/python-docx 等文档工具
# 与 qemu-guest-agent。chv-sync 是仓库内脚本,版本随 repo_commit。
BASE_CONTENTS = [
"git", "openssh-server", "openssh-client", "cloud-init",
"python3", "python3-pip", "curl", "ca-certificates",
"xz-utils", "fonts-wqy-microhei", "fontconfig", "codex",
"codex-code-mode-host", "codex-resources/bwrap",
"/app/host.py", "gt", "chv-sync",
# 对齐 Muse P2:nspawn 单元启动器 + broker + 浏览器子代理。
"systemd-container", "chv-cell", "chv-broker", "chv-browser",
# 对齐 Muse P4:单元外 CDP broker + 浏览器安全共享模块 +
# 静态拦截名单。
"chv-cdp-broker", "chv-browser-guard", "browser-blocklist",
]
# 执行器默认内核 cmdline(config.py),构建产物须与它一起记录(B-12),
# 否则换宿主/换执行器配置时无法复现。
KERNEL_CMDLINE = "console=hvc0 root=/dev/vda1 rw"
# 浏览器装在系统路径(B-22),不是某用户的 ~/.cache:构建时
# PLAYWRIGHT_BROWSERS_PATH 指这里,agent 运行时也须设同一变量。
PLAYWRIGHT_BROWSERS_PATH = "/usr/local/share/ms-playwright"
def _read_lines(path):
try:
with open(path) as fh:
return [line.strip() for line in fh if line.strip()]
except OSError:
return []
def _sha256_size(path):
digest = hashlib.sha256()
size = 0
with open(path, "rb") as fh:
for chunk in iter(lambda: fh.read(1 << 20), b""):
digest.update(chunk)
size += len(chunk)
return digest.hexdigest(), size
def build(outdir, tag, ubuntu, codex_version, repo_commit,
ubuntu_image_sha256, playwright, chromium_pinned_rev=None,
extra_contents=None):
disk_path = os.path.join(outdir, "disk.raw")
vmlinux_path = os.path.join(outdir, "vmlinux")
disk_sha, disk_size = _sha256_size(disk_path)
vmlinux_sha, vmlinux_size = _sha256_size(vmlinux_path)
contents = list(BASE_CONTENTS)
# extra_contents:构建时才确定的附加内容(如 P1 的 vsock_notify,
# 文件存在才拷入)。调用方传什么记什么,不编默认值。
for item in extra_contents or []:
if item and item not in contents:
contents.append(item)
versions = {
"dpkg": _read_lines(
os.path.join(outdir, "guest-versions-dpkg.txt")),
"pip": _read_lines(
os.path.join(outdir, "guest-versions-pip.txt")),
"gt": _read_lines(
os.path.join(outdir, "guest-versions-gt.txt")),
}
gt_version = ""
for line in versions["gt"]:
m = re.search(r"(\d+\.\d+\.\d+)", line)
if m:
gt_version = m.group(1)
break
if playwright:
contents += ["playwright", "chromium-headless-shell"]
versions["chromium"] = _read_lines(
os.path.join(outdir, "guest-versions-chromium.txt"))
info = {
"tag": tag,
"ubuntu": ubuntu,
"codex_version": codex_version,
"repo_commit": repo_commit,
"gt_version": gt_version,
"built_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"contents": contents,
"versions": versions,
"ubuntu_image_sha256": ubuntu_image_sha256,
"ubuntu_image_sha256_set": True,
"kernel_cmdline": KERNEL_CMDLINE,
"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},
"vmlinux": {"sha256": vmlinux_sha, "size": vmlinux_size},
},
}
path = os.path.join(outdir, "BUILDINFO.json")
with open(path, "w") as fh:
json.dump(info, fh, indent=2)
fh.write("\n")
return info
def main(argv):
if len(argv) not in (8, 9, 10):
print("用法:buildinfo.py OUT TAG UBUNTU CODEX_VERSION "
"REPO_COMMIT UBUNTU_IMAGE_SHA256 PLAYWRIGHT "
"[CHROMIUM_HEADLESS_SHELL_REV] [EXTRA_CONTENTS]", file=sys.stderr)
return 2
_, outdir, tag, ubuntu, codex_version, repo_commit, \
ubuntu_image_sha256, playwright = argv[:8]
chromium_pinned_rev = argv[8] if len(argv) >= 9 else None
# 第 10 参数:逗号分隔的附加内容(如 vsock_notify),可空。
extra_contents = None
if len(argv) == 10 and argv[9]:
extra_contents = [s for s in argv[9].split(",") if s]
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",
chromium_pinned_rev, extra_contents)
if not info["versions"]["dpkg"]:
print("BUILDINFO versions.dpkg 为空", file=sys.stderr)
return 1
if not info["versions"]["pip"]:
print("BUILDINFO versions.pip 为空", file=sys.stderr)
return 1
if not info["gt_version"]:
print("BUILDINFO gt_version 为空(gt 未装好)", file=sys.stderr)
return 1
if playwright == "1" and not info["versions"].get("chromium"):
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
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv))