#!/bin/bash # chv-guest 轻量镜像构建脚本(2026-11-06 用户定:停掉 5.5G 全量版, # 按轻量口径重开;不再对齐 Manus,不做三面板/多租户/生产级全套)。 # # 底座:Ubuntu Minimal 24.04 官方 qcow2(248MB, # https://cloud-images.ubuntu.com/minimal/releases/noble/release/ # 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,仅此一个文件——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(见下注)。 # 控制面只留最小的:现有 cloud/chv/executor.py 起停 VM + SSH # 进去调 codex + Playwright 截图。 # # Gitea 双向同步(用户工作区口径,2026-11-06 追加):镜像里加 # git + gt 1.0.0(官方 install.sh + SHA256 校验,跟 170/172 # 同款),配好 git user 与 credential helper 占位,不含真实 # token;客机启动时 /usr/local/bin/chv-sync boot 按 seed 盘 # /etc/chv/guest-env(0600)把用户选定的仓 clone 到 # ~/workspace,后续 pull/push 由用户显式同步,分叉/冲突时 # 失败关闭(不 --force、不自动 rebase)。token 绝不进镜像、 # 代码、日志。 # # 对齐 Muse P2(2026-11-07):镜像里加 systemd-nspawn 运行时单元 #(chv-cell:codex 跑在单元内)+ 单元外 broker(chv-broker: #Unix socket + SO_PEERCRED 白名单代办)+ 浏览器子代理(chv-browser: #单元外独立进程,只返回无障碍树快照)。boot clone 仍在单元外做; #单元隔离口径见 docs/chv-muse-p2-gap.md。 # # 用法(必须在带 /dev/kvm 的宿主上跑全量模式): # CODEX_BIN_DIR=bin UBUNTU_VERSION=24.04 CODEX_VERSION=0.164.0-alpha.9 \ # REPO_COMMIT=$(git rev-parse --short HEAD) \ # UBUNTU_IMAGE_SHA256= ./build.sh out/ # ./build.sh --dry-run out/ # 只打印命令,不执行、不要求 KVM # # 产物(out/ 内): # vmlinux # virt-get-kernel 从云镜像取出的内核(CHV --kernel 指向它) # disk.raw # qemu-img 转成的 raw 根文件系统镜像(CHV_ROOTFS 指向它) # BUILDINFO.json # {tag, ubuntu, codex_version, repo_commit, built_at, contents} # + versions {dpkg, pip, chromium, gt}(构建时从客机读回的实际版本)+ # ubuntu_image_sha256_set(SHA256 必填,见下) # 绝不进镜像的东西:任何模型 key、系统 key、用户账号数据、 # 生产凭证;模型 key 由客机 cloud-init 预置的运行环境变量名 # 注入,不写进镜像层。 # feat-chv-secfix(M4/M8):UBUNTU_IMAGE_SHA256 必填; # sshd 加固由构建写入 /etc/ssh/sshd_config.d/99-chv.conf # 并经 sshd -t + grep 断言(README 的“只允许 key 登录” # 从此是构建断言,不再只是文档声称)。 # B-10:virt-customize 的 chroot 里 sshd -t 须先 ssh-keygen -A # 临时生成 host key 并 mkdir -p /run/sshd,否则分别报无 host # key / 缺 privilege separation 目录而失败;断言通过后立即删 # 临时 key,镜像仍不带 host key(per-VM 预置才生效)。 # # 客机 host key 硬性要求(见 README §5,补缺口轨新增): # 客机必须装 cloud-init 且支持 NoCloud seed,首启先应用 # seed 再启动 sshd,且不得无条件重新生成 host key,否则 # 执行器的 per-VM 预置钉死即失效。本脚本在镜像里写 # /etc/cloud/cloud.cfg.d/99-chv-hostkey.cfg:ssh_deletekeys: # false(绝不删已有 key)+ 首启 bootcmd 只在缺 key 时 # ssh-keygen -A(bootcmd 跑在 seed write_files 之前,预置 # key 随后覆盖生成值,最终仍是预置 key)。 # B-23:曾写 ssh_genkeytypes: [],彻底关掉首启生成;Minimal # 底座 cloud.cfg 又不含 ssh 模块,客机无 host key、sshd -t # 失败、ssh.service 连败,SSH 全程不通——[] 已删,绝不再写。 # # 注:Minimal 底座虚拟盘 3.5G(根分区 2.4G,实测 1.9G 空闲), # qemu-img convert 出的 disk.raw 表观 3.5G、实际占用约 0.6G # (稀疏)。raw 表观 ≤2G 须在构建后另做一步收窄(guestfish # 缩根文件系统 + sgdisk 重建 1 号分区 + qemu-img resize # --shrink),不在本脚本内做——收窄前先确认装完后根分区实际 # 占用,避免把镜像收到无法启动;qcow2 压缩产物是首要验收项。 set -euo pipefail DRY_RUN=0 if [ "${1:-}" = "--dry-run" ]; then DRY_RUN=1 shift fi OUT="${1:-out}" : "${CODEX_BIN_DIR:?CODEX_BIN_DIR 未设:需 npm 安装 @openai/codex 后的配对二进制目录}" : "${UBUNTU_VERSION:?UBUNTU_VERSION 未设}" : "${CODEX_VERSION:?CODEX_VERSION 未设}" : "${REPO_COMMIT:?REPO_COMMIT 未设:构建时仓库 commit 短 hash}" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_DIR="${REPO_DIR:-$(cd "$SCRIPT_DIR/.." && pwd)}" # gt 1.0.0:官方 install.sh(gitea.com/gitea/cli,main 分支 # scripts/install.sh),脚本本身下载 dl.gitea.com 的二进制并 # 验 SHA256;此处再验脚本自身的 SHA256。2026-11-07 06:44 # 上游轮换过一次(V-9 实测:WSL fresh curl 与失败镜像内下载残留 # 逐字节一致,新 sha 49ffe090…;新脚本全文已读,只从 dl.gitea.com # 取 1.0.0 二进制并验 hash,行为干净)——旧值 cf82… 已失效, # 现钉死新值;上游再轮换时构建会再次明确失败,不静默跑新脚本。 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:-49ffe0909be9117e36cf7c8a35b5937939639090322063cbad7667e0bde7c9e9}" # B-03/B-04:输入格式校验(干跑与全量都做,报错点提前, # 不等到 curl 404 或 sha256sum -c 才失败)。 if ! printf '%s' "$UBUNTU_VERSION" | grep -Eq '^[0-9]+\.[0-9]+$'; then echo "UBUNTU_VERSION 格式须为 NN.NN:$UBUNTU_VERSION" >&2; exit 1 fi if ! printf '%s' "$REPO_COMMIT" | grep -Eq '^[0-9a-f]{7,40}$'; then echo "REPO_COMMIT 须为 7-40 位 hex:$REPO_COMMIT" >&2; exit 1 fi if ! printf '%s' "$CODEX_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+'; then echo "CODEX_VERSION 格式异常:$CODEX_VERSION" >&2; exit 1 fi if ! printf '%s' "$GT_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then echo "GT_VERSION 格式异常:$GT_VERSION" >&2; exit 1 fi if ! printf '%s' "$GT_INSTALL_SH_SHA256" | grep -Eq '^[0-9a-f]{64}$'; then echo "GT_INSTALL_SH_SHA256 须为 64 位 hex" >&2; exit 1 fi if [ -n "${UBUNTU_IMAGE_SHA256:-}" ] && ! printf '%s' "$UBUNTU_IMAGE_SHA256" | grep -Eq '^[0-9a-f]{64}$'; then echo "UBUNTU_IMAGE_SHA256 须为 64 位 hex" >&2; exit 1 fi # B-09:版本声称须核对,不只进 tag。codex 二进制真实版本 # 须包含 CODEX_VERSION;REPO_COMMIT 须等于仓库 HEAD 短 hash; # 工作树脏即拒绝构建(tag 会谎称干净 commit)。 CODEX_ACTUAL_VERSION="$("$CODEX_BIN_DIR/codex" --version 2>/dev/null || true)" case "$CODEX_ACTUAL_VERSION" in *"$CODEX_VERSION"*) ;; *) echo "codex 二进制版本与 CODEX_VERSION 不符:got [$CODEX_ACTUAL_VERSION] want 含 [$CODEX_VERSION]" >&2; exit 1 ;; esac HEAD_SHORT="$(git -C "$REPO_DIR" rev-parse --short HEAD)" if [ "$HEAD_SHORT" != "$REPO_COMMIT" ]; then echo "REPO_COMMIT 与仓库 HEAD 不符:$REPO_COMMIT != $HEAD_SHORT" >&2; exit 1 fi if [ -n "$(git -C "$REPO_DIR" status --porcelain)" ]; then echo "仓库工作树有未提交修改,拒绝构建(tag 会谎称干净 commit)" >&2; exit 1 fi TAG="chv-guest-${UBUNTU_VERSION}-${CODEX_VERSION}-${REPO_COMMIT}" UBUNTU_IMAGE_URL="${UBUNTU_IMAGE_URL:-https://cloud-images.ubuntu.com/minimal/releases/noble/release/ubuntu-${UBUNTU_VERSION}-minimal-cloudimg-amd64.img}" run() { if [ "$DRY_RUN" = "1" ]; then echo "DRY-RUN: $*" else echo "+ $*" "$@" fi } # 参数校验(干跑与全量都做,不依赖 KVM)。 for bin in codex codex-code-mode-host; do if [ ! -x "$CODEX_BIN_DIR/$bin" ]; then echo "缺少配对二进制:$CODEX_BIN_DIR/$bin,构建失败" >&2 exit 1 fi done if [ ! -d "$CODEX_BIN_DIR/codex-resources" ]; then echo "缺少 codex-resources(bwrap 载体),构建失败" >&2 exit 1 fi 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 echo "缺少 chv-sync.sh(Gitea 双向同步脚本):$SCRIPT_DIR" >&2 exit 1 fi # 对齐 Muse P2:单元启动器 / broker / 浏览器子代理三脚本缺一即失败。 for p2_script in chv-cell.py chv-broker.py chv-browser.py; do if [ ! -f "$SCRIPT_DIR/$p2_script" ]; then echo "缺少 $p2_script(Muse P2 客机脚本):$SCRIPT_DIR" >&2 exit 1 fi done # 对齐 Muse P3:authd 三件套缺一即失败(与 build.sh 同目录,P2 # 同款规矩)。云端叶子模块(cloud/chv/authd.py 与 sentinel.py) # 不做硬要求:M4 既定“/app 最小集、build.sh 不再要求 cloud/” #(test_chv_residual.BuildScriptMinimalAppTest 钉住),缺失时只 # 警告、不失败——缺叶子模块的镜像里 chv-authd 起不来,chv-sync # 按 P3 差距表 §3.5 回落旧落盘路径并警告,绝不静默假装有 authd。 for p3_file in authd.py git-credential-chv-authd chv-authd.service; do if [ ! -f "$SCRIPT_DIR/$p3_file" ]; then echo "缺少 $p3_file(Muse P3 客机文件):$SCRIPT_DIR" >&2 exit 1 fi done P3_LIBS_PRESENT=0 if [ -f "$REPO_DIR/cloud/chv/authd.py" ] && [ -f "$REPO_DIR/cloud/chv/sentinel.py" ] && [ -f "$REPO_DIR/cloud/__init__.py" ] && [ -f "$REPO_DIR/cloud/chv/__init__.py" ]; then P3_LIBS_PRESENT=1 else echo "警告:Muse P3 云端叶子模块不在仓库里(cloud/chv/authd.py 与 sentinel.py),镜像暂不含 authd 叶子模块" >&2 fi # 对齐 Muse P4:CDP broker / guard 共享模块 / 拦截名单三件缺一即失败。 for p4_file in chv-cdp-broker.py chv_browser_guard.py browser-blocklist.txt; do if [ ! -f "$SCRIPT_DIR/$p4_file" ]; then echo "缺少 $p4_file(Muse P4 客机文件):$SCRIPT_DIR" >&2 exit 1 fi done if [ ! -f "$SCRIPT_DIR/chv-memory.py" ]; then echo "缺少 chv-memory.py(客内记忆文件脚本):$SCRIPT_DIR" >&2 exit 1 fi if [ "$DRY_RUN" = "0" ]; then if [ ! -e /dev/kvm ]; then echo "CHV_GUEST_BUILD needs a KVM host: /dev/kvm 不存在" >&2 exit 1 fi for tool in curl qemu-img virt-customize virt-get-kernel python3 virt-cat; do if ! command -v "$tool" >/dev/null 2>&1; then echo "缺少构建工具:$tool,构建失败" >&2 exit 1 fi done fi run mkdir -p "$OUT" # B-07:磁盘空间预检(仅全量模式;干跑不建目录,无法 df)。 # 轻量版实际占用约 2G(底座 248M + disk.raw 稀疏约 0.6G + # 内核约 46M + 构建余量),阈值从全量版 20G 降到 8G。 if [ "$DRY_RUN" = "0" ]; then avail_kb="$(df -k --output=avail "$OUT" | tail -1 | tr -d ' ')" if [ "${avail_kb:-0}" -lt 8388608 ]; then echo "磁盘不足:$OUT 所在盘可用 ${avail_kb}KB,需 ≥8GB" >&2; exit 1 fi fi IMG="$OUT/ubuntu-${UBUNTU_VERSION}-minimal-cloudimg-amd64.img" # feat-chv-secfix(M4):云镜像 SHA256 改为必填(干跑只验 # “已设”,不验文件;全量模式下载后验哈希)。未设即失败, # 不再只依赖 HTTPS 传输层作唯一锚。 if [ -z "${UBUNTU_IMAGE_SHA256:-}" ]; then echo "UBUNTU_IMAGE_SHA256 未设:云镜像须以哈希锚定,构建失败" >&2 exit 1 fi if [ "$DRY_RUN" = "1" ]; then echo "DRY-RUN: curl -fL --retry 3 --retry-all-errors --connect-timeout 30 $UBUNTU_IMAGE_URL -o $IMG" echo "DRY-RUN: [ -s $IMG ] || exit 1" echo "DRY-RUN: sha256sum -c (UBUNTU_IMAGE_SHA256)" else if [ ! -s "$IMG" ]; then curl -fL --retry 3 --retry-all-errors --connect-timeout 30 "$UBUNTU_IMAGE_URL" -o "$IMG" fi [ -s "$IMG" ] || { echo "下载的云镜像为空:$IMG" >&2; exit 1; } # B-06:哈希不符时删文件重下一次,不直接退出让操作员手动清理。 if ! echo "$UBUNTU_IMAGE_SHA256 $IMG" | sha256sum -c -; then echo "云镜像哈希不符,删除后重下一次:$IMG" >&2 rm -f "$IMG" curl -fL --retry 3 --retry-all-errors --connect-timeout 30 "$UBUNTU_IMAGE_URL" -o "$IMG" [ -s "$IMG" ] || { echo "重下的云镜像为空:$IMG" >&2; exit 1; } echo "$UBUNTU_IMAGE_SHA256 $IMG" | sha256sum -c - fi fi run qemu-img convert -f qcow2 -O raw "$IMG" "$OUT/disk.raw" # 轻量版不扩盘:Minimal 底座根分区 2.4G、实测 1.9G 空闲, # 轻量清单(见下)装完仍有余量;全量版 B-16 的 +12G 扩盘 # 已删除——扩盘会把 raw 表观顶到 16G,与 raw ≤2G 目标直接冲突。 # B-21:apt 必须 --no-install-recommends。virt-customize # --install 默认装 Recommends,python3-pip 会连带拉入 # python3-dev → gcc/g++/build-essential 全套工具链,根分区 # 2.4G 被顶到 96%(实测 2.2G/95M 空闲),Playwright 下载 # headless-shell 到 70% 时 ENOSPC 失败(round 6)。改走 # --run-command apt-get install --no-install-recommends, # 并把会被 Recommends 带进来的必需项显式列出:fontconfig # (字体发现,没有它 Chromium 找不到中文字体)、openssh-client # (git 的 Recommends,agent 任务可能要 ssh 出站)。 # apt-get clean 只清 .deb 缓存、不动 lists(playwright # --with-deps 稍后还要 apt 装浏览器依赖);lists 在最后一步清。 # B-22:virt-customize 的客机 shell 里 HOME=/,Playwright 默认 # 缓存路径 ~/.cache/ms-playwright 会落到根目录 /.cache(round 7 # 实测:浏览器装在 /.cache/ms-playwright/chromium_headless_shell-1248, # /root/.cache 根本不存在,ls 读回空文件、BUILDINFO.versions.chromium # 为空,check_lockfile 兜底报未覆盖)。改走系统路径 # PLAYWRIGHT_BROWSERS_PATH=/usr/local/share/ms-playwright:构建时 # 装这里、读回这里;运行时 agent 也须设同一变量,否则 chv 用户 # 会在自己家目录下找不到浏览器。 # Playwright 默认开启(PLAYWRIGHT=0 可显式关掉),浏览器只装 # chromium-headless-shell(不要完整 Chromium)。单次 # virt-customize:apt 安装、pip、拷入、sshd 加固、git 占位、 # gt 安装、chv-sync、版本读回都在同一次定制里按参数顺序跑; # Playwright 装在第一次 freeze 之后、最终 freeze 之前(B-08: # 最终 freeze 须在 Playwright 安装之后,否则 # BUILDINFO.versions.pip 里没有 playwright;Chromium 版本以 # ms-playwright 缓存目录名记录(如 chromium_headless_shell-1248), # 写进 /app/guest-versions-chromium.txt)。 GT_INSTALL_CMD="curl -fsSL -o /tmp/gt-install.sh https://gitea.com/gitea/cli/raw/branch/main/scripts/install.sh && echo \"${GT_INSTALL_SH_SHA256} /tmp/gt-install.sh\" | sha256sum -c - && GITEA_CLI_VERSION=${GT_VERSION} GITEA_CLI_INSTALL_DIR=/usr/local/bin sh /tmp/gt-install.sh && rm -f /tmp/gt-install.sh && /usr/local/bin/gt version" VC_ARGS=(-a "$OUT/disk.raw" --run-command 'apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends git openssh-server openssh-client cloud-init python3 python3-pip curl ca-certificates xz-utils fonts-wqy-microhei fontconfig systemd-container iproute2 iptables && apt-get clean' --copy-in "$CODEX_BIN_DIR/codex:/usr/local/bin/" --copy-in "$CODEX_BIN_DIR/codex-code-mode-host:/usr/local/bin/" --copy-in "$CODEX_BIN_DIR/codex-resources:/usr/local/bin/" --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 "$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' --copy-in "$SCRIPT_DIR/chv-cell.py:/usr/local/bin/" --copy-in "$SCRIPT_DIR/chv-broker.py:/usr/local/bin/" --copy-in "$SCRIPT_DIR/chv-browser.py:/usr/local/bin/" --run-command 'mv /usr/local/bin/chv-cell.py /usr/local/bin/chv-cell && mv /usr/local/bin/chv-broker.py /usr/local/bin/chv-broker && mv /usr/local/bin/chv-browser.py /usr/local/bin/chv-browser && chmod +x /usr/local/bin/chv-cell /usr/local/bin/chv-broker /usr/local/bin/chv-browser' --mkdir /usr/local/lib/chv-authd --mkdir /usr/local/lib/chv-authd/cloud --mkdir /usr/local/lib/chv-authd/cloud/chv --copy-in "$SCRIPT_DIR/authd.py:/usr/local/bin/" --copy-in "$SCRIPT_DIR/git-credential-chv-authd:/usr/local/bin/" --copy-in "$SCRIPT_DIR/chv-authd.service:/etc/systemd/system/" --run-command 'mv /usr/local/bin/authd.py /usr/local/bin/chv-authd && chmod +x /usr/local/bin/chv-authd /usr/local/bin/git-credential-chv-authd' --run-command 'systemctl enable chv-authd || true' --run-command 'printf "runcmd:\n - systemctl restart ssh || service ssh restart || true\n - systemctl start chv-authd || 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' --copy-in "$SCRIPT_DIR/chv-cdp-broker.py:/usr/local/bin/" --copy-in "$SCRIPT_DIR/chv_browser_guard.py:/usr/local/bin/" --copy-in "$SCRIPT_DIR/browser-blocklist.txt:/usr/local/share/chv/" --run-command 'mv /usr/local/bin/chv-cdp-broker.py /usr/local/bin/chv-cdp-broker && chmod +x /usr/local/bin/chv-cdp-broker && chmod 0644 /usr/local/bin/chv_browser_guard.py /usr/local/share/chv/browser-blocklist.txt' --copy-in "$SCRIPT_DIR/chv-memory.py:/usr/local/bin/" --run-command 'mv /usr/local/bin/chv-memory.py /usr/local/bin/chv-memory && chmod +x /usr/local/bin/chv-memory' --run-command 'git config --system user.name chv && git config --system user.email chv@localhost && git config --system credential.helper store && git config --system init.defaultBranch main' --run-command "$GT_INSTALL_CMD" --run-command '/usr/local/bin/gt version > /app/guest-versions-gt.txt' --run-command 'printf "ssh_deletekeys: false\nbootcmd:\n - \"[ -f /etc/ssh/ssh_host_ed25519_key ] || ssh-keygen -A\"\n" > /etc/cloud/cloud.cfg.d/99-chv-hostkey.cfg; systemctl enable ssh || true' --run-command 'printf "PasswordAuthentication no\nPermitRootLogin prohibit-password\nChallengeResponseAuthentication no\nKbdInteractiveAuthentication no\nPubkeyAuthentication yes\n" > /etc/ssh/sshd_config.d/99-chv.conf && mkdir -p /run/sshd && ssh-keygen -A && sshd -t && grep -q "^PasswordAuthentication no" /etc/ssh/sshd_config.d/99-chv.conf && grep -q "^PermitRootLogin prohibit-password" /etc/ssh/sshd_config.d/99-chv.conf && rm -f /etc/ssh/ssh_host_* && test -z "$(ls /etc/ssh/ssh_host_* 2>/dev/null)"' --run-command 'dpkg-query -W -f "\${Package} \${Version}\n" > /app/guest-versions-dpkg.txt; pip3 freeze > /app/guest-versions-pip.txt || true' --run-command 'useradd --system --home-dir /var/lib/chv-browser --shell /usr/sbin/nologin chv-browser && mkdir -p /var/lib/chv-browser /run/chv/shots && chown chv-browser:chv-browser /var/lib/chv-browser /run/chv/shots && chmod 0755 /run/chv /run/chv/shots' --run-command 'printf "[Unit]\nDescription=CHV cell broker (Muse P2)\nAfter=network.target\n\n[Service]\nType=simple\nExecStart=/usr/bin/python3 /usr/local/bin/chv-broker\nRestart=on-failure\nRestartSec=2\n\n[Install]\nWantedBy=multi-user.target\n" > /etc/systemd/system/chv-broker.service' --run-command 'printf "[Unit]\nDescription=CHV browser sub-agent (Muse P2)\nAfter=network.target\n\n[Service]\nType=simple\nUser=chv-browser\nGroup=chv-browser\nExecStart=/usr/bin/python3 /usr/local/bin/chv-browser\nRestart=on-failure\nRestartSec=2\n\n[Install]\nWantedBy=multi-user.target\n" > /etc/systemd/system/chv-browser.service' --run-command 'printf "[Unit]\nDescription=CHV CDP broker (Muse P4)\nAfter=network.target\n\n[Service]\nType=simple\nUser=chv-browser\nGroup=chv-browser\nExecStart=/usr/bin/python3 /usr/local/bin/chv-cdp-broker\nRestart=on-failure\nRestartSec=2\n\n[Install]\nWantedBy=multi-user.target\n" > /etc/systemd/system/chv-cdp-broker.service' --run-command 'printf "d /run/chv 0755 root root -\nd /run/chv/shots 0755 chv-browser chv-browser -\nd /run/chv/ask-queue 0750 chv-browser chv-browser -\n" > /etc/tmpfiles.d/chv.conf && systemctl enable chv-broker chv-browser chv-cdp-broker || true') # P3 交接(Muse P3 的 authd/sentinel 叶子模块):文件在仓库里存在时 # 拷入镜像(本分支已有,合入后自动生效);不存在时只警告、不失败—— # M4 既定 build.sh 不硬要求 cloud/,见上。缺叶子模块时 chv-authd # 守护进程起不来,chv-sync 按差距表 §3.5 回落旧路径并警告。 if [ "$P3_LIBS_PRESENT" = "1" ]; then VC_ARGS+=(--copy-in "$REPO_DIR/cloud/chv/authd.py:/usr/local/lib/chv-authd/cloud/chv/" --copy-in "$REPO_DIR/cloud/chv/sentinel.py:/usr/local/lib/chv-authd/cloud/chv/" --copy-in "$REPO_DIR/cloud/__init__.py:/usr/local/lib/chv-authd/cloud/" --copy-in "$REPO_DIR/cloud/chv/__init__.py:/usr/local/lib/chv-authd/cloud/chv/") fi # P1 交接(Muse P1 的 vsock_notify.py):文件在仓库里存在时拷入 # 镜像(P1 分支才有,本分支尚无);不存在时只警告、不失败—— # 合并后自动生效,P2 不改该脚本内容。 VSOCK_NOTIFY_PRESENT=0 if [ -f "$REPO_DIR/cloud/chv/guest/vsock_notify.py" ]; then VC_ARGS+=(--copy-in "$REPO_DIR/cloud/chv/guest/vsock_notify.py:/usr/local/bin/" --run-command 'mv /usr/local/bin/vsock_notify.py /usr/local/bin/vsock-notify && chmod +x /usr/local/bin/vsock-notify') VSOCK_NOTIFY_PRESENT=1 else echo "警告:cloud/chv/guest/vsock_notify.py 不在仓库里(P1 未合并),镜像暂不含 vsock notify" >&2 fi if [ "${PLAYWRIGHT:-1}" = "1" ]; then VC_ARGS+=( --run-command '(pip3 install --no-cache-dir --break-system-packages playwright || pip3 install --no-cache-dir playwright) && PLAYWRIGHT_BROWSERS_PATH=/usr/local/share/ms-playwright python3 -m playwright install --with-deps chromium-headless-shell' --run-command 'dpkg-query -W -f "\${Package} \${Version}\n" > /app/guest-versions-dpkg.txt; pip3 freeze > /app/guest-versions-pip.txt || true' --run-command 'ls /usr/local/share/ms-playwright/ > /app/guest-versions-chromium.txt 2>/dev/null || true') fi # B-21 收尾:清掉两轮 apt 的缓存与 lists(--with-deps 装完后 # 不再有 apt 活动),把空间还给镜像。 VC_ARGS+=( --run-command 'apt-get clean && rm -rf /var/lib/apt/lists/*') run virt-customize "${VC_ARGS[@]}" # B-18:virt-get-kernel 的 -o 是输出目录(不是文件),它在 # 目录里下载 vmlinuz- 与 initrd.img-;旧写法 # -o "$OUT/vmlinux" 会建出 vmlinux 目录,后面的 test -s 文件 # 校验必然失败。改成 -o "$OUT",再把 vmlinuz-* 移为 vmlinux # 文件;initrd.img-* 留在 OUT 里作配套产物。另:第三轮真机 # 构建里 virt-get-kernel 首跑报过一次 "file receive cancelled # by daemon",同一盘手动重跑即成功(瞬时故障),此处失败重跑 # 一次,与 B-05/B-06 的重试口径一致。 if [ "$DRY_RUN" = "1" ]; then echo "DRY-RUN: virt-get-kernel -a $OUT/disk.raw -o $OUT" echo "DRY-RUN: mv $OUT/vmlinuz-* $OUT/vmlinux" else run virt-get-kernel -a "$OUT/disk.raw" -o "$OUT" \ || run virt-get-kernel -a "$OUT/disk.raw" -o "$OUT" mv "$OUT"/vmlinuz-* "$OUT/vmlinux" fi # feat-chv-secfix(M4):构建时把客机内实际装好的版本 # 读回 BUILDINFO.versions(dpkg + pip freeze),名字列表 # 之外再有逐项版本——可审计、可复现比对。事前 lockfile # 钉死(apt/pip 精确版本预先锁定)仍待构建宿主生成, # 见 README 与就绪清单 §11;本步先保证“建成什么版本 # 都有记录”,不再只有名字。 if [ "$DRY_RUN" = "1" ]; then echo "DRY-RUN: virt-cat -a $OUT/disk.raw /app/guest-versions-dpkg.txt > $OUT/guest-versions-dpkg.txt" echo "DRY-RUN: virt-cat -a $OUT/disk.raw /app/guest-versions-pip.txt > $OUT/guest-versions-pip.txt" echo "DRY-RUN: virt-cat -a $OUT/disk.raw /app/guest-versions-gt.txt > $OUT/guest-versions-gt.txt" if [ "${PLAYWRIGHT:-1}" = "1" ]; then echo "DRY-RUN: virt-cat -a $OUT/disk.raw /app/guest-versions-chromium.txt > $OUT/guest-versions-chromium.txt" fi else virt-cat -a "$OUT/disk.raw" /app/guest-versions-dpkg.txt > "$OUT/guest-versions-dpkg.txt" virt-cat -a "$OUT/disk.raw" /app/guest-versions-pip.txt > "$OUT/guest-versions-pip.txt" virt-cat -a "$OUT/disk.raw" /app/guest-versions-gt.txt > "$OUT/guest-versions-gt.txt" || true if [ "${PLAYWRIGHT:-1}" = "1" ]; then virt-cat -a "$OUT/disk.raw" /app/guest-versions-chromium.txt > "$OUT/guest-versions-chromium.txt" || true fi 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} ${CHROMIUM_HEADLESS_SHELL_REV:-} [vsock_notify?] # 生成 $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; } if [ "${PLAYWRIGHT:-1}" = "1" ] && [ -z "$CHROMIUM_HEADLESS_SHELL_REV" ]; then echo "警告:CHROMIUM_HEADLESS_SHELL_REV 未设,Chromium 版本只记录、不钉死(V-9 验收须设该变量重建)" >&2 fi EXTRA_CONTENTS="" [ "$P3_LIBS_PRESENT" = "1" ] && EXTRA_CONTENTS="cloud/chv/authd.py,cloud/chv/sentinel.py" [ "$VSOCK_NOTIFY_PRESENT" = "1" ] && EXTRA_CONTENTS="${EXTRA_CONTENTS:+$EXTRA_CONTENTS,}vsock_notify" python3 "$SCRIPT_DIR/buildinfo.py" "$OUT" "$TAG" "$UBUNTU_VERSION" "$CODEX_VERSION" "$REPO_COMMIT" "$UBUNTU_IMAGE_SHA256" "${PLAYWRIGHT:-1}" "${CHROMIUM_HEADLESS_SHELL_REV:-}" "$EXTRA_CONTENTS" fi echo "tag=$TAG" echo "out=$OUT" if [ "$DRY_RUN" = "1" ]; then echo "干跑结束:未执行任何构建步骤,镜像未建成" else echo "构建完成:$OUT/vmlinux $OUT/disk.raw $OUT/BUILDINFO.json" fi