# 部署前聚合体检与备份合同自动刷新:实施计划 ## 0. 边界与回滚点 - [x] 用户批准本规划后才运行 `task.py start`;批准规划不等于批准生产安装。 - [x] 实施阶段只修改仓库代码、测试、spec/runbook;不连接生产、不改 root `authorized_keys`、不安装 forced-command、不部署。 - [x] 保留 `deploy.sh` 首次初始化/应急兼容;`deploy-existing.sh` 的自动强制启用与生产 forced-command 安装安排在另行批准的同一窗口。 - [x] 不删除旧源码目录、旧环境文件、旧 unit 或备份制品。 ## 1. 共享合同读取与聚合体检 - [x] 提取六键 systemd Environment allowlist 的共享只读解析逻辑,供 `deploy-existing.sh` 和新体检入口使用;任何输出不包含值。 - [x] 新增聚合器与固定检查表,主流程不用 `set -e`,每项子 shell 隔离、显式收集状态,最后统一返回。 - [x] 实现普通部署身份的仓库根、可写性、env 可读性、PGDATA/附件和运行容器 mount/build 核对。 - [x] 实现独立 `DEPLOY_REFRESH_SERVER` 的 transport/probe 核对,并把身份不可用与程序不可用分开分类。 - [x] 让 `deploy-existing.sh` 在任何远端写入前要求体检全绿;保留既有 remote preflight。 ## 2. root-owned 自包含刷新程序 - [x] 新增无仓库运行时依赖的 forced-command target:固定 PATH/umask、清理危险环境、精确解析 `SSH_ORIGINAL_COMMAND`。 - [x] `probe` 验证 root 身份、固定工具、root-owned unit 与固定目标前提,只输出固定标识。 - [x] `refresh` 取得固定锁,从 root-owned unit 与唯一 running server 解析活动 build,拒绝调用方路径/SHA/unit 输入。 - [x] 以原子 root-owned systemd drop-in 覆盖四个 service 的两项 build 环境,daemon-reload 后验证最终 SHA。 - [x] 验证四个 timer enabled/active;用稳定退出码区分 unit 未刷新和 timer 未运行。 - [x] 不调用 repository installer,不使用 `eval`、动态 shell、动态 unit 名或部署用户可写可执行文件。 ## 3. forced-command 安装器 - [x] 将 target 安装到固定 `/usr/local/sbin`,验证 root ownership、mode 和非 symlink。 - [x] 读取显式专用公钥,拒绝同 key blob 的不受限 root 授权;只原子维护自身 managed authorized_keys 行并保留其他 key。 - [x] forced-command 选项禁用 agent/port/X11/PTY/user-rc,安装完成后执行本地静态合同检查;不把 installer 本身加入免密或 forced-command。 - [x] 增加卸载/回滚说明,但不在无独立批准时自动删除授权行或 drop-in。 ## 4. 部署后接线与状态表达 - [x] `deploy-existing.sh` 只在 `deploy.sh` 完整成功后调用 `refresh`。 - [x] 成功时才打印完整部署完成;失败时固定输出 `DEPLOYED_BUT_CONTRACT_REFRESH_FAILED`、四类之一并退出非零,不转发敏感 stderr。 - [x] 不增加 optional/跳过刷新开关;新版已有部署入口缺少 `DEPLOY_REFRESH_SERVER` 即在写前阻塞。生产窗口先安装并 probe forced-command,再运行新版 wrapper。 - [x] 更新 runbook/checklist,明确两个 SSH 身份、forced-command 专用 key、部署前聚合报告、后刷新非零语义和生产安装单独批准。 ## 5. 测试与门禁 - [x] 新增聚合器 fixtures:多个同时失败、首项失败仍执行末项、依赖失败、固定输出顺序和最终退出码。 - [x] 新增脱敏 fixtures:底层输出含伪造路径/主机/env/容器 ID 时,外层报告不泄露。 - [x] 新增 forced-command fixtures:空命令、未知命令、附加参数、probe、refresh、锁、原子 drop-in、四 service SHA 与四 timer。 - [x] 覆盖四类后刷新失败:root transport、程序 126/127、unit SHA、timer 状态;断言 wrapper 非零且不打印完整成功。 - [x] 扩展 deployment safety contract,锁定体检在 mkdir/rsync 前,refresh 在 `/ready`、smoke 和公网检查后,且 helper 不执行仓库路径。 - [x] 运行: - `bash -n`(所有新增/修改 shell) - `node deploy/trialrun/validate-env-contract.js` - `cd server && npm test -- tests/unit/scripts/deployment-safety-contract.test.js` - 新增聚合/forced-command fixture runner - `bash scripts/qa/run-automatic-backup-contract.sh` - `python3 ./.trellis/scripts/task.py validate 08-02-deployment-preflight-backup-contract-refresh` - Trellis governance、process contract、docs registry、sensitive scan、`git diff --check` ## 6. 审查、提交与生产门 - [x] 实施后执行只读安全/部署 reviewer,重点核查 forced-command key 是否真正受限、root target 是否执行部署用户可写内容、聚合器是否短路、状态分类是否准确;reviewer 不改代码。 - [x] 用 `trellis-update-spec` 更新 deployment-trialrun 的稳定合同,避免只把知识留在 task。 - [ ] 中文提交并引用 `08-02-deployment-preflight-backup-contract-refresh`;不自动提交、不自动部署。 - [ ] 代码获批后另行申请生产窗口:先安装 forced-command/probe,再启用 wrapper 强制刷新,执行一次真实部署或受控无业务写入验证;任何 root/authorized_keys 写入和部署均需单独明确批准。 本地验证(2026-08-02):聚合/forced-command fixture 通过,覆盖 13 项汇总、多个并发失败不短路、脱敏输出、四类收尾映射、精确命令白名单、四个 build drop-in、四个 timer、managed key 更新及不受限重复 key 拒绝;deployment safety Vitest 9 passed;环境合同通过;automatic-backup 合同 73 checks passed;全部 shell/Node 语法通过。只读复查发现 forced-command target 缺少运行时自身 ownership/mode/path 校验,已补 `validate_self()`;最终复核又将 systemd build 校验从子串匹配收紧为精确键与最后覆盖值,并用 `OLD_BUILD_SHA` 回归证明不会静默误判。相关聚合、环境合同和 Vitest 均复跑通过;复查未发现新的 root 提权、动态执行或部署顺序 blocker。生产未连接、未安装、未部署。