Files
codex-app-server/chv-guest/generate_lockfile.py
T
host 0ebcdfb2a7 fix(chv-muse-r12fix-4): detect_checkout 接进 CDP broker(P10)
R12 真机:detect_checkout 在 chv-guest/、host.py、
executor.py 里零引用,结账页无任何拦截。按 P10 gap §4.1
接线:broker 加载 cloud/chv/checkout_detect.py(同目录
优先,仓库路径兜底;缺失时降级关闭检测、写警告,绝不
crash-loop);snapshot() 每次重算 verdict(aria 行拼伪节点,
与 _form_risk 同形),open() 导航前后两道门——落点是结账
页时返回 form_submit ask 并暂停,上一页是结账页时下一次
导航先审批;ask 批过后同 URL 不再重复 ask(批准即放行,
与 form_risk 同口径);verdict 进 snapshot()/status() 回包。
build.sh:checkout_detect.py 条件拷入 /usr/local/bin/(0644,
P3/P6 同款只警告口径,M4 不硬要求 cloud/),
extra_contents 与两处 lockfile 登记补齐。钱包三腿不在本轮
范围,未动。
2026-11-08 04:31:36 +00:00

243 lines
9.5 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 客机镜像的供应链 lockfile。
事后校验见 check_lockfile.py(校验 BUILDINFO.versions);
本脚本是生成端:读 BUILDINFO.json,以其中的 versions
(build.sh 已从客机读回的 dpkg-query / pip freeze,
Playwright=1 时另有 chromium 标识)为唯一版本来源,
逐项给 contents 清单配版本,写出 lockfile.json:
{"tag": ..., "image_sha256": ... | null,
"generated_at": UTC ISO 时间,
"versions": {"dpkg": ["name version", ...],
"pip": ["name version", ...],
"declared": ["name version", ...]},
"unresolved": ["查不到版本的名", ...]}
版本来源映射(绝不编造版本号,查不到的进 unresolved):
- dpkg 包名 → versions.dpkg("name version" 行)
- pip 包名 → versions.pip("name==version" 行)
- codex / codex-code-mode-host / codex-resources/bwrap
→ BUILDINFO.codex_version(随 codex 一起发布)
- /app/host.py → BUILDINFO.repo_commit
- chromium / chromium-headless-shell → versions.chromium
(ms-playwright 缓存目录名)
注意:旧版逐项查构建宿主的 dpkg/pip,方法不成立——宿主
版本与客机版本没有必然关系,且 /app/host.py、codex 三件
套在宿主上永远查不到,--verify 永远过不了。本版只读
BUILDINFO.versions,不再现场查宿主。
用法(均在构建宿主上跑):
python3 generate_lockfile.py BUILDINFO.json --output lockfile.json
python3 generate_lockfile.py --verify lockfile.json \
--buildinfo BUILDINFO.json
"""
import json
import sys
from datetime import datetime, timezone
# 非包名的内容项:版本来自 BUILDINFO 的声明字段,不是 dpkg/pip。
DECLARED_SOURCES = {
"codex": "codex_version",
"codex-code-mode-host": "codex_version",
"codex-resources/bwrap": "codex_version",
"/app/host.py": "repo_commit",
"gt": "gt_version",
"chv-sync": "repo_commit",
# 对齐 Muse P2:nspawn 单元启动器 + broker + 浏览器子代理,
# 均是仓库内脚本,版本随 repo_commit(与 chv-sync 同款)。
"chv-cell": "repo_commit",
"chv-broker": "repo_commit",
"chv-browser": "repo_commit",
# 对齐 Muse P3 + P1 vsock:authd 三件套、authd/sentinel
# 叶子模块与 vsock 通知脚本,均是仓库内脚本,版本随
# repo_commit。缺了它们 generate 会记 6 项 unresolved,
# verify 与 check 双双失败(717efc4 真机实测)。
# round-11 真机修复(层 1):叶子树新增 errors.py 与 stub
# __init__.py(authd_init_stub.py),同款 repo_commit 登记,
# 缺了它们新镜像的 lockfile 同样会 unresolved。
"chv-authd": "repo_commit",
"git-credential-chv-authd": "repo_commit",
"chv-authd.service": "repo_commit",
"cloud/chv/authd.py": "repo_commit",
"cloud/chv/sentinel.py": "repo_commit",
"cloud/chv/errors.py": "repo_commit",
"cloud/chv/__init__.py": "repo_commit",
"vsock_notify": "repo_commit",
# 对齐 Muse P4:CDP broker 三件在 contents 里,登记自 P4 起
# 一直缺失;本轮补上,否则新镜像的 lockfile 仍 unresolved。
"chv-cdp-broker": "repo_commit",
"chv-browser-guard": "repo_commit",
"browser-blocklist": "repo_commit",
# 对齐 Muse P6:worker 三件 + 叶子模块(privsep/safety)。
"chv-gitea-worker": "repo_commit",
"chv-sync-worker-client": "repo_commit",
"chv-gitea-worker.service": "repo_commit",
"cloud/chv/privsep.py": "repo_commit",
"cloud/chv/checkout_detect.py": "repo_commit",
"cloud/chv/safety.py": "repo_commit",
}
def _index_dpkg(lines):
"""dpkg 行 "name version" → {小写名: "name version"}。"""
index = {}
for line in lines or []:
parts = str(line).split(None, 1)
if len(parts) == 2:
index[parts[0].lower()] = "%s %s" % (parts[0], parts[1])
return index
def _index_pip(lines):
"""pip freeze 行 "name==version" → {小写名: "name version"}。"""
index = {}
for line in lines or []:
if "==" in str(line):
name, version = str(line).split("==", 1)
index[name.lower()] = "%s %s" % (name, version.strip())
return index
def _index_chromium(lines):
"""chromium 标识行 → 键。完整 Chromium 的目录名是
chromium-<rev>,headless-shell 的是 chromium_headless_shell-<rev>,
各自映射到同名 contents 项。"""
index = {}
for line in lines or []:
text = str(line).strip()
if text.startswith("chromium_headless_shell"):
index["chromium-headless-shell"] = \
"chromium-headless-shell %s" % text
elif text:
index["chromium"] = "chromium %s" % text
return index
def generate(buildinfo_path, output_path):
with open(buildinfo_path) as fh:
buildinfo = json.load(fh)
names = buildinfo.get("contents") or []
if not isinstance(names, list):
print("BUILDINFO.contents 不是列表", file=sys.stderr)
return 2
versions = buildinfo.get("versions") or {}
dpkg_index = _index_dpkg(versions.get("dpkg"))
pip_index = _index_pip(versions.get("pip"))
chromium_index = _index_chromium(versions.get("chromium"))
dpkg, pip, declared, unresolved = [], [], [], []
for name in names:
key = name.lower()
if name in DECLARED_SOURCES:
version = buildinfo.get(DECLARED_SOURCES[name])
if version:
declared.append("%s %s" % (name, version))
else:
unresolved.append(name)
continue
if key in chromium_index:
declared.append(chromium_index[key])
continue
if key in dpkg_index:
dpkg.append(dpkg_index[key])
continue
if key in pip_index:
pip.append(pip_index[key])
continue
unresolved.append(name)
lockfile = {
"tag": buildinfo.get("tag"),
"image_sha256": buildinfo.get("image_sha256")
or buildinfo.get("sha256"),
"generated_at": datetime.now(timezone.utc).isoformat(),
"versions": {"dpkg": sorted(dpkg), "pip": sorted(pip),
"declared": sorted(declared)},
"unresolved": sorted(unresolved),
}
with open(output_path, "w") as fh:
json.dump(lockfile, fh, indent=2, sort_keys=True)
fh.write("\n")
if unresolved:
print("警告:%d 项查不到版本,已列入 unresolved:%s"
% (len(unresolved), ", ".join(sorted(unresolved))),
file=sys.stderr)
return 0
def verify(lockfile_path, buildinfo_path):
with open(lockfile_path) as fh:
lockfile = json.load(fh)
if buildinfo_path:
with open(buildinfo_path) as fh:
buildinfo = json.load(fh)
if lockfile.get("tag") != buildinfo.get("tag"):
print("tag 不一致:lockfile %s != BUILDINFO %s"
% (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)
if not entries:
print("versions.%s 为空,供应链清单缺失" % kind,
file=sys.stderr)
return 1
for entry in entries:
parts = str(entry).split()
if len(parts) < 2 or not parts[1]:
print("版本为空的条目:%s %r" % (kind, entry),
file=sys.stderr)
return 1
unresolved = lockfile.get("unresolved") or []
if unresolved:
print("清单不完整,unresolved 非空:%s" % sorted(unresolved),
file=sys.stderr)
return 1
print("lockfile 校验通过:tag=%s dpkg=%d pip=%d declared=%d"
% (lockfile.get("tag"), len(versions["dpkg"]),
len(versions["pip"]),
len(versions.get("declared") or [])))
return 0
def main(argv):
if "--verify" in argv:
lockfile_path = argv[argv.index("--verify") + 1]
buildinfo_path = None
if "--buildinfo" in argv:
buildinfo_path = argv[argv.index("--buildinfo") + 1]
return verify(lockfile_path, buildinfo_path)
if len(argv) < 2:
print("用法:generate_lockfile.py BUILDINFO.json "
"--output lockfile.json | --verify lockfile.json "
"[--buildinfo BUILDINFO.json]", file=sys.stderr)
return 2
output_path = None
if "--output" in argv:
output_path = argv[argv.index("--output") + 1]
if not output_path:
print("缺少 --output", file=sys.stderr)
return 2
return generate(argv[1], output_path)
if __name__ == "__main__":
raise SystemExit(main(sys.argv))