固化部署体检与备份合同刷新(08-02-deployment-preflight-backup-contract-refresh)

这个提交包含在:
2026-08-02 11:59:42 +08:00
父节点 ea985de975
当前提交 7138d2b85c
共修改 18 个文件,包含 1321 行新增和 34 行删除
@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
@@ -0,0 +1,120 @@
# 部署前聚合体检与备份合同自动刷新:设计
## 1. 边界与组件
本任务只改已有部署入口,不改变首次初始化状态机:
1. 本地只读聚合体检入口读取受控 unit 六键,并分别通过普通部署 SSH 与专用 forced-command SSH 验证远端事实。
2. `deploy-existing.sh` 在体检全绿后继续调用既有 `deploy.sh`;`deploy.sh` 的 fail-fast remote preflight 保留。
3. 应用、公网检查全部通过后,`deploy-existing.sh` 调用 root forced-command 的 `refresh`。
4. root-owned 自包含程序只维护四个固定 service 的 build drop-in,并验证四个既有 timer。
不新增后台服务、数据库对象、业务 API 或业务页面。
## 2. 聚合体检执行模型
### 2.1 入口与输入
- 普通部署目标继续使用 `DEPLOY_SERVER`。
- 独立维护目标使用新的显式 `DEPLOY_REFRESH_SERVER`;格式使用现有安全 SSH target 字符集,不从主机名、用户名或别名规则猜测。
- 六键仍以 automatic-backup daily unit 的 allowlisted `Environment=` 为事实源,不回显值。
- 共享只读解析函数可由体检与 `deploy-existing.sh` 复用,避免两份六键 allowlist 漂移;不得把解析结果写入仓库或临时报告。
### 2.2 不短路聚合器
主脚本保留 `set -uo pipefail`,明确不启用 `set -e`。每项使用以下等价模型:
```bash
run_check() {
local check_name="$1"
shift
if ("$@") >/dev/null 2>&1; then
record_result "$check_name" PASS
else
record_result "$check_name" FAIL
fi
}
```
检查函数在子 shell 中运行,不能修改主流程状态。聚合器记录所有结果后统一输出固定顺序的 `检查项 PASS/FAIL` 和计数,最后按总失败数返回 0/非 0。依赖项无法取得时,依赖检查返回 FAIL;无依赖检查仍继续执行。
### 2.3 检查分组
- 本地/合同:普通 SSH transport、六键齐全、root SSH transport、forced-command probe。
- 普通部署身份:仓库根标识、目录可写、env 普通文件且可读、PGDATA/附件存在。
- 运行时:唯一 server/db、活动 build、附件 mount、PGDATA mount。
- 备份合同:daily unit build 等于活动 build。
- root 收尾能力:forced-command 程序可执行并返回固定 probe 标识。
所有检查仅把 stdout/stderr 捕获到内存并丢弃;用户可见输出由聚合器根据固定结果生成,不转发底层错误文本。
## 3. forced-command 安全模型
### 3.1 密钥边界
- 使用独立公钥。安装器解析 key blob/fingerprint,若 root `authorized_keys` 已存在同 key blob 的非 managed、非 forced-command 行则拒绝安装。
- managed 行使用固定标记,可原子替换自身,保留其他 root 管理 key;不得覆盖整个 `authorized_keys`。
- 授权选项固定为 `command="<root-owned-target>",no-agent-forwarding,no-port-forwarding,no-X11-forwarding,no-pty,no-user-rc`。
- 专用本地 SSH alias 只使用该 key。普通 root 应急管理 key 不属于部署自动化合同。
### 3.2 自包含程序
- 安装器把审核后的源文件复制到固定 `/usr/local/sbin` 目标,验证 root:root、`0755`、regular file、非 symlink。
- 程序启动后设置固定 `PATH`、`umask 077`,清理调用方可控的路径/loader 类环境变量,仅读取 root-owned systemd unit、Docker inspect 结果和 `SSH_ORIGINAL_COMMAND`。
- 精确命令矩阵:`probe` 只验证运行前提;`refresh` 执行 build drop-in 更新;空值、空白变体、额外参数和其他命令全部返回固定 unsupported 分类。
- 程序不得 source 或 exec 仓库内容,不接受 argv、stdin 配置、路径、unit、SHA 或 shell 片段。
## 4. build 刷新合同
### 4.1 事实读取
1. 从 root-owned automatic-backup daily unit 读取固定 allowlist:Compose project 与当前工作目录/env 合同只用于定位唯一活动 server。
2. 通过 Docker labels/inspect 定位唯一 running server,读取容器 `BUILD_SHA`。
3. build 必须为完整小写十六进制提交标识;前端标识固定取前 12 位,不接受调用方输入。
### 4.2 最小写入
- 固定目标仅为 automatic-backup daily/watchdog/drill-reminder 与 WARCH3 四个 service。
- 对每个 service 原子安装 root-owned `20-active-build.conf` drop-in,只包含 `BUILD_SHA` 和 `VITE_BUILD_SHA` 覆盖;不改主 unit、WorkingDirectory、env 路径、ExecStart 或 timer 配置。
- 所有 drop-in 写入完成后执行一次 `systemctl daemon-reload`。
- 读取 systemd 最终合并后的 Environment,逐个断言两个 SHA;再逐个断言四个 timer `is-enabled=enabled` 且 `is-active=active`。
- 使用固定 root-owned lock 防止两个 refresh 并发。中途失败不得报告成功;已写 drop-in 不自动回滚到旧 SHA,因为旧 SHA 已与活动应用不一致,人工修复应继续向活动 build 收敛。
## 5. 调用顺序与失败语义
```text
聚合体检全绿
-> deploy.sh(同步、构建、迁移、/ready、smoke、公网检查)
-> forced-command refresh
-> unit SHA + timer 验证
-> 部署完整完成
```
- 体检失败:零远端写入,部署退出非零。
- `deploy.sh` 失败:保持既有发布错误语义,不调用 refresh。
- refresh transport 失败:固定分类 `root_identity_unavailable`。
- remote command 126/127 或 probe 标识缺失:固定分类 `refresh_program_unavailable`。
- helper 返回/复核 SHA 不一致:`unit_build_not_refreshed`。
- SHA 已一致但任一 timer 非 enabled/active:`timer_not_active`。
- 后刷新失败时应用已部署且 smoke 通过,包装脚本输出固定 `DEPLOYED_BUT_CONTRACT_REFRESH_FAILED` 并退出非零;不伪装回滚,也不打印底层路径/响应。
## 6. 安装与发布边界
- 代码合入不自动修改生产 root 文件或 `authorized_keys`。
- 首次安装分两步:root 运行安装器;普通身份与 forced-command 身份分别执行 probe。两步均需生产写入批准和脱敏证据。
- 新版 `deploy-existing.sh` 对已有部署直接要求 `DEPLOY_REFRESH_SERVER`,缺失即在写前体检失败,不保留可长期绕过的 optional 模式。生产切换顺序固定为:先用旧运行版本不受影响的方式安装 forced-command 并完成 probe,再执行新版 wrapper;远端入口尚未安装时不得开始同步。
- 回滚代码时可恢复旧 `deploy-existing.sh`;root drop-in 仍指向当前活动 build,不影响服务启动。删除 forced-command 或 drop-in 属于单独 root 运维动作。
## 7. 测试设计
- SSH fixture 必须区分 transport 255、远端 126/127、helper 固定业务退出码和成功标识。
- 聚合测试故意让第一个、第三个检查失败,断言最后一个检查仍执行且输出顺序固定。
- 脱敏测试向底层 stderr 注入伪造路径、主机、secret、容器 ID,断言用户输出不包含它们。
- helper fixture 使用隔离的 fake systemctl/docker/install,不触碰真实 `/etc`;生产程序固定 PATH,测试通过源码级显式 test harness 注入依赖,安装目标不携带测试覆盖开关。
- 静态合同断言 installed helper 不引用 `REMOTE_DIR` 下脚本,不使用 `eval`/`bash -c`/动态 unit 名,不读取调用方 SHA。
## 8. 取舍与延期
- 选择 systemd drop-in 而非重新调用 installer:只写每次部署必变的 build 字段,避免执行部署用户可写源码,也避免复制两套完整 unit 生成逻辑。
- 选择 forced-command 专用 key 而非 sudoers:授权面只包含 probe/refresh;代价是需要一次独立 key 和 root authorized_keys 安装。
- 不加每周 timer:当前漂移已完成一次性全量对账,先用真实部署观察是否仍有持续漂移来源。
@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
@@ -0,0 +1,64 @@
# 部署前聚合体检与备份合同自动刷新:实施计划
## 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。生产未连接、未安装、未部署。
@@ -0,0 +1,92 @@
# 部署前聚合体检与备份合同自动刷新
## Goal
让已有部署在任何远端写入前一次性看清合同、权限、数据挂载和备份构建身份的全部漂移,并在应用切换成功后自动刷新备份/WARCH3 service 的构建标识。部署不再依赖操作者逐个撞 fail-fast 护栏,也不再依赖人工记住“部署后重装 timer”。
## Background
- 2026-08-02 的受控部署连续暴露三个历史漂移断面:冻结的 `REMOTE_DIR` 不是仓库根、root 维护身份与部署身份未按实际通路区分、冻结的环境文件路径不可由部署用户读取。现有护栏串行 fail-fast,因此每修复一处才看到下一处。
- 同一轮恢复已完成一次全量对账:六键合同、163 个生产环境键、运行容器、PGDATA/附件挂载、4 个 service、4 个 timer、真实备份和异机恢复点均已对齐并跑绿。本任务把该对账方法变成可重复机制。
- `deploy.sh` 只把新 `BUILD_SHA` 注入应用容器,不重装 automatic-backup/WARCH3 units;备份 runner 又要求 unit 中冻结的 build 与活动容器一致。人工漏做刷新时,应用可正常运行而自动备份静默 fail-closed。
- 仓库已有 forced-command 先例:异机备份接收端以 root-owned 可执行文件配合 `SSH_ORIGINAL_COMMAND` 白名单和 SSH 转发/PTY 禁用选项限制授权面。
## Scope
- 为已有部署增加一次性汇总的只读体检,并在任何远端写入前阻断全部已发现的合同、权限、挂载和 build 漂移。
- 建立独立 forced-command SSH 身份与 root-owned 自包含刷新程序,只更新四个固定 service 的 build drop-in并验证四个既有 timer。
- 把聚合体检接到 `deploy-existing.sh` 前置,把自动刷新接到完整部署与公网检查之后,并补齐测试、runbook、checklist 和部署 spec。
- 保留 `deploy.sh` 首次初始化/应急路径和既有 installers;生产安装、authorized_keys 修改和首次启用另行批准。
## Requirements
### 1. 只读聚合体检
- 新增已有部署专用体检入口,由 `deploy-existing.sh` 在任何 `mkdir`、rsync、Compose、systemd 或其他远端写入前调用。
- 体检至少覆盖:六键合同完整;`REMOTE_DIR` 是完整仓库根且部署用户可写;`REMOTE_ENV_FILE` 为普通文件且部署用户可读;PGDATA 含 `PG_VERSION`;附件目录存在;运行中唯一 server/db 容器与合同中的 build、PGDATA、附件挂载一致;automatic-backup unit 的 `BUILD_SHA` 与活动 server 一致;普通部署 SSH 身份可用;独立 root forced-command 身份可用且收尾程序存在、可执行。
- 主流程不得使用 `set -e` 实现检查遍历。每个检查必须在独立子 shell 中执行并显式收集退出码;前一项失败不能阻止无依赖的后续项执行。依赖事实缺失时,相关项仍以固定检查名报告 FAIL,不得静默跳过。
- 输出只包含固定检查项名、`PASS/FAIL`、汇总计数和固定错误分类;不输出 SSH 别名、主机、路径、env 值或散列、容器 ID、密钥、unit 原文及原始命令响应。
- 任一必需检查失败,体检最终退出非零,`deploy-existing.sh` 不得继续同步或部署。现有 fail-fast `preflight-remote.sh` 保留为第二道防线。
### 2. 独立 root forced-command 通路
- 使用独立维护 SSH 身份,不以 `sudo -n true` 代表 root 能力,不新增 NOPASSWD sudoers。
- 专用公钥必须只以 forced-command 形式授权;同一 key blob 若还存在不受限 root 授权,安装器必须 fail-closed。不得复用当前不受限 root 管理密钥来制造“看似受限”的入口。
- forced-command 指向 `/usr/local/sbin` 下 root-owned、非符号链接、`0755` 的自包含程序;程序不得 source、exec 或调用 `REMOTE_DIR` 及其他部署用户可写目录中的脚本、二进制或配置。
- SSH 入口禁用 agent forwarding、端口转发、X11、PTY 和 user rc;只接受 `SSH_ORIGINAL_COMMAND` 的精确白名单子命令,首版仅允许无附加参数的 `probe` 与 `refresh`。
### 3. 部署后自动刷新
- `deploy-existing.sh` 只有在应用部署、`/ready`、runtime/external smoke 与公网检查成功后,才通过独立 root forced-command 身份调用 `refresh`。
- root 程序从 root-owned systemd 合同和唯一活动 server 容器读取 Compose 身份及完整 `BUILD_SHA`,严格校验后仅更新四个固定 service 的 build 环境覆盖:automatic-backup daily、watchdog、drill-reminder 与 WARCH3。不得接受调用方传入路径、unit 名、SHA 或任意命令。
- 刷新必须原子写入 root-owned systemd drop-in,执行 `daemon-reload`,再验证四个 service 的最终 `BUILD_SHA/VITE_BUILD_SHA` 与活动容器一致,以及四个既有 timer 均为 enabled/active。不得调用仓库内 installer,也不得顺带重写其他 unit 字段。
- 刷新失败时 `deploy-existing.sh` 必须退出非零并明确标记“应用已部署但合同未收口”,不得打印普通成功结论,也不得自动回滚已通过 smoke 的应用。
- 对外固定区分四类失败:root SSH 身份不可用;收尾程序不存在或不可执行;程序完成但 unit SHA 未更新;unit 已更新但 timer 未 enabled/active。
### 4. 安装、兼容与生产边界
- 提供一次性 root 安装器,将经过审核的自包含程序复制为 root-owned 文件并安装/更新 managed forced-command 授权行;安装器本身仍需显式 root 执行,不进入免密授权。
- 保留现有 automatic-backup/WARCH3 installers 作为首次安装、路径/完整合同变化和人工修复入口;自动收尾只覆盖每次发布必变的 build 标识。
- 现有 `deploy.sh` 首次初始化/受控应急入口不强制依赖 forced-command;自动体检与刷新只接入 `deploy-existing.sh`。
- 生产安装 forced-command、修改 root `authorized_keys` 或首次启用自动收尾均属于生产写入,实施完成后必须另行批准;本任务规划批准和代码批准不授权远端安装。
## Acceptance Criteria
- [ ] 合成场景同时制造至少两个前置失败时,体检报告包含全部预期 FAIL 项,且最后一个独立检查确实执行;不能因 `set -e`、pipefail 或函数非零提前退出。
- [ ] 体检全绿时只输出固定项名和 PASS/汇总;敏感扫描与测试证明没有路径、主机、env 值/散列、容器 ID 或原始响应。
- [ ] 六键缺失、仓库根错误、env 不可读、源码根不可写、PGDATA/附件缺失、实际挂载漂移、unit build 漂移均能在远端写入前一次汇总发现。
- [ ] 普通部署身份和 root forced-command 身份分别验证;root transport 不可用与远端程序不存在/不可执行得到不同固定分类。
- [ ] forced-command 只接受精确 `probe`、`refresh`;空命令、带额外参数、其他子命令均拒绝。安装器拒绝同一公钥同时存在不受限 root 授权。
- [ ] 安装后的 root 程序为 root-owned、`0755`、非符号链接,且静态/动态测试证明不执行仓库或其他部署用户可写路径中的程序。
- [ ] 成功 `refresh` 从活动容器取得 build,原子更新四个固定 service 的 drop-in,四个最终 SHA 一致且四个 timer enabled/active;调用方无法指定 SHA、路径或 unit。
- [ ] 四类收尾失败分别有回归:SSH root 身份不可用、程序不存在/不可执行、unit SHA 未更新、timer 未运行。
- [ ] 任一收尾失败发生在应用已切换后,`deploy-existing.sh` 退出非零并输出固定“已部署但未收口”状态;成功时才输出部署完整完成。
- [ ] `deploy.sh` 直接初始化/应急路径保持兼容;automatic-backup/WARCH3 既有 installer、真实备份与 watchdog 行为不被改写。
- [ ] shell syntax、部署安全单测、环境合同、聚合体检 fixtures、forced-command fixtures、Trellis/governance/process/sensitive/diff 门禁全部通过。
## Out of Scope
- 不建设 CI/CD、蓝绿发布、自动回滚或新的通用远程执行框架。
- 不把体检挂成每周 timer;先观察它在后续真实部署中是否持续发现漂移,再单独评估第 5 个 unit。
- 不在本任务删除四个旧源码目录、旧环境文件或其他历史回退材料;体检跑绿也只形成后续清理的申请前提。
- 不通过 `SYNC_ENV=1` 修复环境文件,不自动挑选、复制或轮换生产 Secret。
- 不让 root 收尾程序修改 Compose、数据库、附件、业务数据、备份制品或执行真实恢复。
## Deferred Decision With Trigger
- 每周只读体检 timer 暂不建设。触发条件:本脚本上线后在后续真实部署中再次发现非一次性漂移,或出现人工改服务器导致合同在部署间隔内失配;届时单独立项评估定时运行、告警和第 5 个 unit 的维护成本。
- 旧目录/旧环境文件清理单独审批。至少要求聚合体检在规范根上跑绿、一次后续真实部署自动收尾跑绿,且当前异机备份仍新鲜。
## Test Plan
- `bash -n` 检查新增/修改的所有部署 shell。
- 扩展 `server/tests/unit/scripts/deployment-safety-contract.test.js`,锁定 preflight 在任何远端写入前、刷新在应用与公网检查后、失败必须向上传播。
- 新增纯 fixture 合同测试,使用假的 SSH/systemctl/docker 响应覆盖聚合不短路、脱敏输出、身份/程序四类失败、固定 unit/drop-in 和 timer 验证。
- 运行 `node deploy/trialrun/validate-env-contract.js`、相关 automatic-backup 合同测试及既有部署安全回归。
- 运行 Trellis task validate、governance、process contract、docs registry、sensitive content 与 `git diff --check`。
## Notes
- Keep `prd.md` focused on requirements, constraints, and acceptance criteria.
- Lightweight tasks can remain PRD-only.
- For complex tasks, add `design.md` for technical design and `implement.md` for execution planning before `task.py start`.
@@ -0,0 +1,26 @@
{
"id": "deployment-preflight-backup-contract-refresh",
"name": "deployment-preflight-backup-contract-refresh",
"title": "部署前聚合体检与备份合同自动刷新",
"description": "新增只读聚合部署体检,并通过独立 forced-command root 身份自动刷新部署后的备份构建合同。",
"status": "in_progress",
"dev_type": "infra",
"scope": "deploy/trialrun 与部署安全合同",
"package": "hyb-oa",
"priority": "P2",
"creator": "shunli",
"assignee": "shunli",
"createdAt": "2026-08-02",
"completedAt": null,
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}