diff --git a/.trellis/spec/hyb-oa/deployment-trialrun.md b/.trellis/spec/hyb-oa/deployment-trialrun.md index 523de906..7e5b26a5 100644 --- a/.trellis/spec/hyb-oa/deployment-trialrun.md +++ b/.trellis/spec/hyb-oa/deployment-trialrun.md @@ -1344,6 +1344,10 @@ validate_copied_batch(private/batch, source_metadata) - Candidate render: `bash scripts/qa/run-offhost-shadow-restore-runner-contract.sh render `; it prints only PASS and the candidate SHA-256, never config values or paths. +- One-shot installer manifest: the candidate `LIB_DIR`, runner entry, dependency + basenames and sudoers entry form one exact private mapping. The installer must + validate that mapping before its first privileged write; a sibling `lib` / + `libexec` path reconstructed independently is not equivalent. ### 3. Contracts @@ -1410,6 +1414,17 @@ validate_copied_batch(private/batch, source_metadata) `server/prisma/migrations` at the source commit, validates every directory name, sorts the names and hashes that exact non-empty set. Files such as `migration_lock.toml` are not migrations and must never enter the fingerprint. +- A one-shot installer must install every helper into the exact `LIB_DIR` + embedded in the already approved rendered runner, or derive both values from + the same validated private render input and compare them before `sudo + install`. It must use the same exact destinations for preflight, installation, + residue accounting and cleanup; a path that differs only by `lib` versus + `libexec` is an entry-contract failure, not an ARM environment exception. +- A root/sudo helper must not use `$HOME` to locate a caller-owned upload, + installer or diagnostic script: sudo normally changes it to the root home. + Self-cleanup may use `$0` only after validating the fixed basename, regular + file type and absence of a symlink; otherwise a non-root outer wrapper owns + cleanup of the exact caller-side path. ### 4. Validation & Error Matrix @@ -1447,6 +1462,12 @@ validate_copied_batch(private/batch, source_metadata) hashing; never reinterpret the extra file as schema drift. - Candidate differs from the deterministic render or has not passed the tracked contract -> do not upload or install it. +- Installer dependency destination differs from the rendered runner `LIB_DIR` + -> fail before the first privileged file write; do not install into the + similar sibling directory and rely on runner cleanup to discover it later. +- A sudo helper resolves caller staging through root `$HOME`, or cannot prove + the fixed identity of `$0` -> leave caller-side cleanup to the non-root outer + wrapper and report it explicitly; do not guess or unlink a different home. ### 5. Good/Base/Bad Cases @@ -1471,6 +1492,12 @@ validate_copied_batch(private/batch, source_metadata) - Bad: add two successful queries only to the fixture while the rendered runner still accepts the first socket `pg_isready`, or use a fixed sleep as an ARM timing workaround. +- Good: the installer mechanically compares its dependency destination with the + rendered runner `LIB_DIR`, installs all three pinned helpers there and checks + that same exact directory during residue accounting. +- Bad: type a site path twice, install helpers below `lib` while the candidate + reads `libexec`, or use sudo `$HOME` to delete a script uploaded by the + calling user. ### 6. Tests Required @@ -1514,6 +1541,14 @@ validate_copied_batch(private/batch, source_metadata) - Directly test source/copy identity, dynamic migration fingerprint and the tracked attachment validator. Run `check-sensitive-content.mjs` explicitly on all three tracked runner assets and require zero findings. +- Build an installer fixture from a rendered candidate and assert runner entry, + exact `LIB_DIR`, dependency basenames and sudoers destination are one mapping. + Mutate only `libexec` to `lib` and require failure before any simulated + privileged install; verify residue accounting uses the same destinations. +- Exercise a diagnostic helper with sudo/root `HOME`: `$0` plus fixed-basename, + regular-file and non-symlink checks may self-clean, while an unverified path + must remain owned by a non-root outer cleanup. Assert no root-home path is + inferred or emitted. ### 7. Wrong vs Correct @@ -1532,10 +1567,12 @@ print result=NO_GO ```text tracked template + exact private config -> deterministic candidate + SHA-256 +candidate LIB_DIR == installer dependency destination -> privileged install contract drives rendered TCP target function -> list -> target -> real restore path continuous stage transitions -> exact stage sum + one data-only RTO capture only stage/path/exit/category/duration allowlists finalize active stage -> emit fixed diagnostics -> finally teardown raw private runtime +sudo helper: validated $0 self-cleanup or exact non-root outer cleanup; never sudo $HOME non-zero apply remains FAIL ``` diff --git a/.trellis/tasks/08-23-isolated-backup-restore-drill/implement.md b/.trellis/tasks/08-23-isolated-backup-restore-drill/implement.md index 83227ca0..f485f6f9 100644 --- a/.trellis/tasks/08-23-isolated-backup-restore-drill/implement.md +++ b/.trellis/tasks/08-23-isolated-backup-restore-drill/implement.md @@ -122,7 +122,7 @@ - [x] “socket 可用但 TCP 不可用仍失败”、首次失败后恢复、streak 重置、名称错配立即失败、永久查询失败有界退出五类回归通过;真实 `restore-db.sh` Docker stdin/host-file、源/副本身份、dynamic migration、附件安全、敏感输出和清理合同继续通过。 - [x] 已审计合同全部覆盖机制,未发现第二套自写等价恢复逻辑;完整 runner 的 migration/12 类表/附件编排仍只能由真实隔离恢复验证,已作为明确限制保留,不能由本地组件合同替代。 - [x] `scripts/restore-db.sh` 相对生产构建字节未改;runner、成对备份/恢复、自动异机备份 74 项、task、Trellis、process、docs、敏感扫描和空白门禁均通过。 -- [ ] 本地修复阶段不连接 ARM、不生成或安装 Attempt 5 现场入口。提交后重新选择最新批次并执行只读探针、重新渲染候选和核对 SHA-256;实际恢复须再次单独批准。 +- [x] 本地修复阶段未连接 ARM、未生成或安装 Attempt 5 现场入口;提交后已重新选择最新批次、执行只读探针、重新渲染候选并核对 SHA-256,实际恢复另行取得了明确批准。 ### Attempt 5 前只读身份探针结果 @@ -138,7 +138,7 @@ - [x] runner 增加固定 allowlist 阶段字段,成功时八段均为非负整数且精确加总为 `data_restore_rto_seconds`;失败时已完成段和当前失败段保留数字,后续段为 `NOT_RUN`。 - [x] 合同注入各阶段成功/失败,验证最长 TCP 轮询只进入目标确认阶段、后续 `NOT_RUN`、总数精确加总、输出不含路径/SQL/凭据;原有清理与恢复合同继续通过。 - [x] 用户批准的当前边界仅为本地修改 runner、更新可复跑合同并执行回归;不连接 ARM、不重新渲染 Attempt 5 现场候选、不执行实际恢复。 -- [ ] 本轮八阶段计时与完整合同通过后,下一份满足 24 小时门禁的新鲜批次直接进入 Attempt 5 的候选渲染、SHA-256 核对、只读探针和实际恢复审批流程。此后发现的改进项,除非会导致 Attempt 5 失败或产生错误结论,否则只登记为后续任务,不再作为 Attempt 5 前置增强插队。 +- [x] 八阶段计时与完整合同通过后,满足 24 小时门禁的批次已直接进入 Attempt 5 候选渲染、SHA-256 核对、只读探针和实际恢复审批流程;期间未插入新的非阻断增强。 - [x] 八阶段状态必须在失败路径清理前输出;阶段边界沿用 `dump_list`、`target_confirmation`、`restore_apply` 等既有诊断词汇,不建立第二套同义命名。 ### Attempt 5 最终候选与执行前探针 @@ -148,7 +148,19 @@ - [x] 最终只读探针重新选择执行时最新正式批次:来源构建与当前生产相同,年龄 `68889` 秒且不超过 24 小时,run id/目录配对、DB/附件完整性和 source digest 均通过。 - [x] 本地复核来源/当前 migration 为 `65/65`、差异 `0`,候选 migration 指纹与来源集合一致,应用代码漂移和 `scripts/restore-db.sh` 字节漂移均为 `0`。 - [x] 探针 root 入口、sudoers 和普通用户暂存残留均为 `0`;本地 runner 合同容器残留为 `0`。本阶段未复制、解包或恢复正式归档,未连接腾讯云生产。 -- [ ] Attempt 5 实际隔离恢复须在新鲜度门禁仍成立时另行批准;runner 会在执行开始重新选择最新正式批次并重算 24 小时门禁,超龄或身份变化均失败关闭。执行证据还必须记录实际选择的批次脱敏身份及其与本次探针批次是否一致,不能默认二者相同。 +- [x] Attempt 5 实际隔离恢复已在新鲜度门禁成立时另行批准并执行;紧邻执行前探针通过,但 runner 在选择实际恢复批次之前即因依赖库入口合同失败,因此本轮不存在“实际选择批次”,不能宣称恢复输入与探针批次相同。 + +### Attempt 5 实际结果与安装器差异调查 + +- [x] 安装前候选 SHA-256 与已批准值精确一致;紧邻执行前只读探针通过,批次年龄 `70444` 秒,来源构建与当前生产相同,run id 配对、产物完整性和 source digest 门禁通过。 +- [x] runner 在 `entry_contract` 阶段以 `restore_library_missing` 失败关闭;批次选择、正式归档复制、PostgreSQL 容器启动、数据库/附件恢复和全部八阶段 RTO 均未运行,原始结果保留为 `NO_GO`。 +- [x] 失败分类为 `installer_path_mismatch`:已批准 runner 从固定 `libexec` 位置加载三项依赖,本轮一次性安装器却把同一组依赖安装到相似但不同的 `lib` 位置。该结论指向演练安装器,不证明 runner 或备份不可用。 +- [x] 对错误安装目录做精确只读检查:三项依赖均为 root 拥有的普通文件、SHA-256 前缀与本地包一致、额外条目数 `0`;随后只清理该已确认目录并得到 `runner_lib_cleanup=PASS`。 +- [x] 诊断脚本在 sudo 上下文中用 `$HOME` 推断普通用户暂存位置,导致自删除误指向 root 家目录;随后由普通用户外层对已知暂存脚本执行精确清理。后续安装器不得在 sudo 脚本中用 `$HOME` 推断调用用户路径。 +- [x] Attempt 5 后腾讯云生产 `/ready` 为 `PASS`,12 位构建前缀仍为 `7b86ffb3c9ef`;失败发生在归档选择之前,未触碰腾讯云生产或正式归档内容。 +- [x] 最终只读复核确认 runner/probe 入口与 sudoers、正确 `libexec`、错误 `lib`、普通用户暂存、Docker 容器和 runtime 六类残留全部为 `0`,dead-man/健康 timer 为 `active/active`;复核脚本自身清理 `PASS`。 +- [x] ignored/workspace 安装器已把三项依赖安装目标对齐 runner 已批准的 `libexec` 合同;诊断脚本改用经 basename、普通文件和非链接校验的调用路径自清理,不再以 sudo `$HOME` 推断普通用户路径。runner 与候选字节未改、无需重新渲染。 +- [ ] 再次实际恢复前重新选择满足 24 小时门禁的新鲜批次,执行只读身份探针并单独申请实际恢复批准;不得复用 Attempt 5 的批次新鲜度或既有实际恢复授权。 ## 验证命令类别 diff --git a/.trellis/tasks/08-23-isolated-backup-restore-drill/research/restore-drill-evidence.md b/.trellis/tasks/08-23-isolated-backup-restore-drill/research/restore-drill-evidence.md index 9b7b1ac5..6076a6f6 100644 --- a/.trellis/tasks/08-23-isolated-backup-restore-drill/research/restore-drill-evidence.md +++ b/.trellis/tasks/08-23-isolated-backup-restore-drill/research/restore-drill-evidence.md @@ -495,3 +495,49 @@ Attempt 5 前增强停止线从本节起生效:八阶段计时完成并通过 候选钉住的是 `EXPECTED_BUILD_PREFIX` 和 `EXPECTED_MIGRATION_SET_SHA`,不是本次探针批次的 source run id。runner 会在 ARM 上于实际执行时重新选择当时最新正式批次,再对实际批次执行构建前缀、目录/run id、三文件集合、大小、SHA-256、RPO 和恢复后 migration 集合门禁。如果探针后有更新批次原子落地,实际恢复输入可以合法切换到更新批次;最终证据必须记录实际批次的脱敏身份,并明确它与本次探针批次是否一致,不能仅因候选 SHA-256 不变就默认恢复了探针批次。 至此 Attempt 5 的前置批次身份、演练器合同、八阶段 RTO 和失败诊断均已具备;最新正式备份是否实际可恢复仍未得到答案,必须由另行批准的 Attempt 5 隔离恢复给出结论。 + +## Attempt 5 实际结果与安装器路径根因 + +日期:2026-08-25 + +用户明确批准 Attempt 5 后,执行方在 ARM 安装前再次核对候选 SHA-256,完整值与已批准候选一致。紧邻执行前的只读探针在演练调用同一秒观测到最新正式批次年龄 `70444` 秒,来源构建与当前生产相同,run id/目录配对、DB/附件产物完整性、source digest 和 24 小时门禁均为 `PASS`。探针反推的批次创建时刻已作为操作侧脱敏时间证据保留。 + +runner 随后在 `entry_contract` 阶段以 `restore_library_missing` 失败关闭:`dump_list`、目标确认和 restore apply 均为 `NOT_RUN`,八个 RTO 阶段与总计也全部为 `NOT_RUN`,最终结果原样保留为 `NO_GO`。失败发生在 runner 选择实际恢复批次之前,因此本轮不存在可对外记录的“实际恢复批次”,也不能用紧邻探针批次代替实际恢复输入。正式归档复制、PostgreSQL 容器启动、数据库恢复、migration/12 类表核对、附件恢复与关联验证均未开始;这次结果没有证明最新备份可恢复,也没有证明备份不可恢复。 + +本次 finding 分类为 `installer_path_mismatch`,属于演练安装器缺陷。已批准 runner 的依赖合同指向固定 `libexec` 目录,本轮一次性安装器却把三个依赖安装到名称相似但不同的 `lib` 目录。runner 因而在正确位置找不到依赖并按入口合同失败关闭;其 `temporary_access_cleanup=PASS` 清理的是合同内目标,而错误目录未被该清理函数命中,这解释了初始 `attempt5_entry_residue=1` 与 runner 自报清理通过并存的原因。 + +对错误目录的定向只读诊断得到:`restore-db.sh`、`restore-storage.sh` 和附件校验器均存在、均为 root 拥有的普通文件,三者 SHA-256 脱敏前缀与本地安装包一致,额外条目数为 `0`。在目标身份和文件集合全部明确后,仅对该错误安装目录执行精确清理,结果 `runner_lib_cleanup=PASS`;该动作未使用宽泛目录匹配,也未触碰正式归档。 + +诊断脚本的自删除另外暴露出一个安装器层问题:脚本在 sudo 上下文中以 `$HOME` 推断普通用户暂存路径,实际解析为 root 家目录,因目标不存在而报 `unlink` 失败。这不影响错误依赖目录已清理的结论;随后由非 root 外层对普通用户暂存中的已知诊断脚本做精确删除。后续安装器不得在 sudo 脚本中使用 `$HOME` 推断调用用户路径,应由安装器预先固定并验证普通用户暂存目标,或由非 root 外层负责自清理。 + +演练后腾讯云生产 `/ready` 由既有审计窄路径只读取得 `PASS`,12 位构建前缀仍为 `7b86ffb3c9ef`,与演练前一致。Attempt 5 未进入正式归档选择和复制路径,也未启动容器或恢复写入,因此腾讯云生产与正式归档未被本次失败修改。 + +在任何下一次实际恢复前仍需做一次最终只读零残留复核,范围为:runner 入口、合同内 `libexec`、错误 `lib`、runner/probe sudoers 及 candidate、probe 入口、普通用户安装器/候选/诊断暂存、Docker 容器和 runtime,并确认 dead-man 与健康 timer 仍为 `active/active`。在该复核完成前,不把 Attempt 5 表述为“全部临时对象零残留”。 + +最小修订方案只涉及 ignored/workspace 安装器:将三项依赖的安装目标从错误 `lib` 对齐到 runner 已批准的 `libexec`,并修正普通用户暂存脚本的清理所有权;不修改已批准 runner,不重新渲染候选,候选 SHA-256 可保持不变。该安装器修订、本地校验、最新批次只读探针和任何再次实际恢复均需按既有批准边界分别推进。核心问题——最新正式备份能否完成数据库与附件隔离恢复——在 Attempt 5 后仍无答案。 + +## Attempt 5 最终零残留复核与安装器最小修订 + +日期:2026-08-25 + +用户批准后,执行方沿既有 ARM 窄路径运行固定目标的最终复核入口。该入口只读取本轮已知入口、依赖目录、sudoers、暂存、容器/runtime 和两个监控 timer 的聚合状态,不读取正式归档或业务数据,不执行恢复。结果如下: + +| 检查项 | 结果 | +| --- | --- | +| runner/probe 入口、sudoers 与 candidate 残留 | `0` | +| runner 合同内 `libexec` 残留 | `0` | +| 安装器误用的 `lib` 残留 | `0` | +| 普通用户安装器/候选/诊断暂存残留 | `0` | +| 本轮容器残留 | `0` | +| 本轮 runtime 残留 | `0` | +| dead-man/健康 timer | `active/active` | +| 最终复核 | `PASS` | +| 复核脚本自身清理 | `PASS` | + +至此 Attempt 5 初始 `entry_residue=1` 已由定向诊断、精确清理和独立最终复核闭环为全部已知临时对象残留 `0`。该清理结论不改变 Attempt 5 的 `NO_GO`,也不补造任何未运行的 RTO、数据库或附件恢复证据。 + +已批准的本地最小修订只改变 ignored/workspace 安装器,不进入 Git 历史:三项依赖安装目标由错误 `lib` 对齐到已批准 runner 中同一 `libexec`,静态检查确认安装器与 rendered runner 的目录类别一致;诊断脚本以自身调用路径作为自清理目标,并在删除前验证固定 basename、普通文件且非链接,不再读取 sudo `$HOME`。三个私有脚本通过 Bash 语法检查和 Git ignore 守卫;已批准 runner、附件校验器、恢复脚本和候选字节均未修改,候选 SHA-256 不变。 + +本地完整复核结果:影子恢复 runner 合同全部 `PASS`,真实 PostgreSQL 16 fixture 的 Docker stdin/host-file 路径继续通过,合成成对备份/恢复演练 `PASS`,自动异机备份合同 `74/74 PASS`;task validate、Trellis governance、process baseline、敏感扫描和 `git diff --check` 通过。文档审计错误 `0`,只保留既有未登记文件 warning;`scripts/restore-db.sh` 相对生产构建字节漂移 `0`,本地测试容器残留 `0`。 + +任何再次实际恢复仍须重新选择满足 24 小时门禁的新鲜批次、重新执行只读身份探针并取得独立批准。本节只闭环 Attempt 5 的环境清理和安装器路径缺陷,不回答最新正式备份是否可恢复。