Skip to content

Validate retained files before static-site rollback - #6213

Merged
huangruiteng merged 1 commit into
loopx-project:mainfrom
Duang777:codex/fix-static-site-revision-boundary-20261011
Oct 11, 2026
Merged

huangruiteng merged 1 commit into
loopx-project:mainfrom
Duang777:codex/fix-static-site-revision-boundary-20261011

Conversation

@Duang777

Copy link
Copy Markdown
Collaborator

Summary

  • validate retained revision file manifests before rollback copies any file
  • reject path traversal, symlinked storage, duplicate or reserved paths, invalid file metadata, and content drift
  • bind rollback metadata to the requested revision and keep the active deployment unchanged on rejection

Root cause

rollback_static_site passed persisted REVISION_FILE paths directly to _copy_declared_files. A modified files[*].path could escape both the retained revision and the staging directory. In a concrete reproduction, ../../escaped.txt overwrote an existing file beside output_dir. Rollback also did not compare retained file bytes with their recorded size and SHA-256 digest.

Validation

  • Red: both new regression tests failed with DID NOT RAISE; the path case overwrote the file outside output_dir.
  • Green: tests/test_static_site_presentation.py passes, 11 tests total.
  • A real CLI rollback now exits 2 with revision file path must stay inside the retained revision, and the outside file remains unchanged.
  • Ruff check and format check pass for both changed files.
  • Project-registry I/O manifest currentness and changed-source semantic inventory pass.
  • loopx check reports a clean public boundary for both changed files.
  • loopx canary premerge --from-git-diff --git-diff-base upstream/main passes both selected canaries.
  • Focused mypy output matches the same 5 existing failures on upstream/main; this change adds none.

Sibling sweep

_copy_declared_files has three callers. Both package callers use the fresh list from _collect_site_files; only rollback consumed persisted metadata without validation. No sibling change is required.

uv.lock is unchanged.

Treat retained revision metadata as untrusted before copying files. Reject paths outside the revision, symlinks, binding mismatches, and content drift before staging a rollback.

Signed-off-by: Duang777 <duangjl007@gmail.com>
@Duang777

Copy link
Copy Markdown
Collaborator Author

@loopx-agent review

@loopx-agent loopx-agent left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewer: model_agent | gpt-6.1-sol | OpenAI | runtime_reported | reasoning_effort=xhigh

Exact reviewed head: 6213@9081da5ee4f294036b082a1c1d5fb9a0ba47ce79; immutable base: dbbca15b5e1848e89a605676c17a28ff47f662d4. 两文件完整 diff、既有生产调用者和保留版本写入器已读;最新主干的相关 owner 没有改变。未发现阻塞项。

动机

回滚已保留静态网站版本的 CLI 操作者。

以前保留版本的文件清单被直接用于复制;路径穿越可覆盖输出目录旁的文件,内容损坏也会被当成成功回滚。现在先验证所选版本,再准备替换;失败保留当前网站,修复后可继续回滚。

独立真实 CLI 复现基线越界写入;head 拒绝且输出、日志和目录外文件不变。合法回滚、预览、新版本延续及修复后恢复成立。

本轮覆盖本地保留版本校验及回滚,不部署外部网站,不认证可由本地写者伪造的摘要或验证声明。

规格依据为 no_spec:先检索上述不可变 base 的文档及现有讨论,没有找到专门的已接受静态网站回滚规格,也没有关联 issue。因此不编造 RFC 验收编号。当前任务来自可独立复现的错误;基线 package_static_site 的写入约定,以及 既有 package/rollback/HTTP smoke 是普通合法调用和恢复行为的参照。

改动思路

先核验目标版本中的既有数据,再复用原来的复制、暂存替换和回执 owner。仅做路径检查仍会放过错误版本绑定、损坏字节或缺失入口;重新扫描整个版本目录又不能代表历史声明的准确文件集合。这次局部验证比新增持久化框架或迁移协议更合适。

Existing packaging produces all fields now checked. Validate the selected persisted input at rollback, reuse copy/replace/receipt owners, and keep the staged replacement after complete validation. A general persistence/authority framework is unnecessary.

当前 PR 的完整边界是:所选保留版本通过校验后才准备替换,并经真实 CLI 验证拒绝、继续回滚和读回;没有新增发布能力。

Python 位于既有本地文件 IO 边界,不新增通用控制面决策源;不需要为这项修复做无关 TS 迁移。新读取器复用 _semantic_digest、_validation_receipt 和保留路径集合,未引入第二份状态或权限规则。

具体改动

关键代码讲解

  • _validate_retained_revision_files(loopx/presentation/static_site.py:254):接收历史文件清单;拒绝绝对/父级路径、重复和保留路径、符号链接、缺失文件以及错误摘要/大小。布尔值不会被误作整数大小。读取真实字节验证后返回规范化清单,整个过程没有写入。
  • rollback_static_site(同文件 :555):CLI 的 rollback 分支进入这里。先核验目标的 site/revision 绑定、语义摘要、声明入口和原有 passed 验证回执,再组装原格式 manifest/receipt。校验发生在 _replace_output 之前,错误走既有 StaticSiteContractError 和 CLI exit 2。
  • _replace_output(同文件 :355):沿用已有暂存构建、替换及替换失败恢复。两处 fresh-package 复制入口继续使用 _collect_site_files 生成的清单,历史 rollback 入口则使用本次验证结果;不把相似复制过程错误合成另一套 authority。

生产文件增加 115 行、删除 6 行;测试增加 62 行,覆盖路径穿越和保留内容损坏。新增 SHA256 正则属于该文件的局部输入校验,已有 schema/version、reserved 集合及验证值均复用,没有新共享词汇。

独立基线/head 真实 main() + 文件系统执行 23 组场景:普通恢复、预览、缺失版本;父级/绝对路径;重复/保留文件;文件、父目录和版本目录符号链接;错误 site/revision;无效摘要、布尔/负数大小;内容/语义摘要损坏;缺失入口和验证回执;错误条目类型和空/非数组清单。Head 的全部预期成立;拒绝时按完整目录树、当前 manifest 和事件日志验证没有写入。基线的 traversal 调用 exit 0 并覆盖目录外测试文件,head exit 2 并保留原内容,证明检查可以识别原缺陷。

继续执行三版本流程:生成 one/two/three;保留 one 的错误 metadata,仍能恢复未受影响的 two;随后恢复新版本 three;修复 one 后再次恢复并分别读取 index.html/manifest。另一个内容损坏案例在 head 拒绝后修复原字节,成功回滚,再继续到原 active 版本。不是只得到一张“拒绝回执”。

对主干的风险

更严格的历史输入拒绝是公开说明的默认变化;基线生产写入器已包含所有新增必检字段,普通既有版本无需迁移。只核验调用者选定的目标,不要求其他保留版本一起重新通过认证。合法预览保持无写入,明确 execute 后的 manifest/receipt/event 结构沿用原 owner。

本地验证:head 11 passed,base 9 passed;两个版本的真实 loopback HTTP smoke 都通过。原生 premerge 5 项 direct、2 项 selected 全通过,包含完整语义检查,manual holds 为 0;两侧 Ruff 均通过。开发期 diff 语义 advisory 在 full 检查之前执行,支持范围候选为 0,不以空结果证明语义等价。未查询、轮询或等待 GitHub CI。

最初测试到达 100% 后清理历史 pytest 临时目录过慢,已中断本轮自己的清理进程,并用独立 --basetemp 重跑相同用例取得完整 exit 0 结果;没有把中断运行算作通过。最初基线反例脚本错误地要求已发生错误回滚的 base 还能按 head 的恢复次序成功,已去掉该假设,保留基线错误行为及 head 恢复读回。

剩余边界:本地可写者仍可共同伪造内容、摘要或 passed 声明;本次结构/完整性验证不是签名认证。没有证明恶意并发写者在检查后替换文件的抗竞态能力,也没有远程部署资格。这些未被本 PR 承诺,不能把它们写成已经通过的验证。没有新的 actor lifecycle、自动执行授权或 default-off capability,也没有第一屏呈现改动。

我的整体评价

APPROVE。这个完整切片通过实际调用阻止越界/损坏恢复,同时保留普通历史版本、预览及后续恢复路径;不以行数、作者自测或测试数量替代结果判断。

未来相关重构检查已执行:保留目录校验属于当前 rollback owner,复用既有摘要、验证及替换函数足够;未发现必须在此 PR 引入的新框架或另一套共享规则。当前比例适当,保留既有持久化格式。批准不授予部署或合并权限。

English verdict: APPROVE — 6213@9081da5ee4f294036b082a1c1d5fb9a0ba47ce79. No blocking finding. The retained-target validator prevents the reproduced baseline path escape and corruption before staging, while actual CLI preview, three-version continuation, unrelated-revision isolation and repair/retry readback remain usable. 11 head/9 base tests, 23 paired cases, both real HTTP smokes, Ruff and all 5 direct/2 selected premerge checks pass. Local hashes/validation receipts are not signed authenticity or a hostile-writer concurrency guarantee; no remote deployment or GitHub CI was consulted.

@huangruiteng
huangruiteng merged commit f7a5df0 into loopx-project:main Oct 11, 2026
23 of 26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants