Repository navigation
fix(canary): keep a passing smoke's verdict when a Windows fixture holds read-only git objects - #6716
JasonBuildAI wants to merge 5 commits into
Conversation
…px-project#6716 Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
9775695 to
d106232
Compare
JasonBuildAI
left a comment
There was a problem hiding this comment.
Author record of the independent review. No self-approval is implied by this comment.
An independent adversarial reviewer — given only the linked issue, the diff, the repro steps and the check commands — reviewed revision 97756955e and returned APPROVE WITH NOTES after reproducing the central before/after at the real entrypoint:
loopx canary smoke-suite --suite full-public --script examples/canary/catalog-planner-smoke.py: basestatus failed/returncode 0/ reasonfixture cleanup failed … [WinError 5] … .git\objects\…→ headstatus passed/failure_count 0.python examples/issue-fix-reviewer-recommendation-smoke.py: base exit 1 with an unhandledPermissionError[WinError 5] → headok, exit 0.python -m pytest tests/test_smoke_suite.py --loopx-smoke-script examples/canary/catalog-planner-smoke.py: base 1 failed / 1 passed (assert False is Trueon the rc-0 payload) → head 2 passed.ruff checkover every touched file:All checks passed!; the required pytest set matches the PR body.- The three
tests/canaryfailures this PR declares pre-existing were reproduced on the untouched base revisionfdf03ec61.
Both findings are fixed at head d1062324c, and the PR body records them:
- The new suites ran on no hosted Windows lane, so the guard was inert where the defect reproduces (they pass trivially on Ubuntu, where unlink ignores the read-only bit).
.github/workflows/python-tests.ymlnow qualifies all three files in thewindows-powershelllane;scripts/ci/test_windows_lane_coverage.pystill passes. tests/canary/test_smoke_fixture_cleanup.pycase 2 monkeypatched_remove_fixture_root, a symbol this PR introduces, so it did not exercise the fix. It now patches the teardown seamremove_owned_treeand pins the fail-closed contract instead, and the tautologicalshutil.rmtreemock test was dropped.
Not reproduced by the reviewer: the 33-check full-public sweep row (failures 20 → 18, checks naming read-only unlink 4 → 0). That row is this author's run on native Windows 11 and stays author-reported evidence.
Re-verified at d1062324c after rebasing onto 555a83df1 (base moved forward since the tested revision): ruff clean; the three focused suites 12 passed / 26 skipped; the lane guard 2 passed / 12 subtests; examples/issue-fix-workflow-e2e-smoke.py and examples/issue-fix-reviewer-recommendation-smoke.py exit 0; the single-check runner verdict status passed / failure_count 0; the pytest facade 2 passed.
…head d106232 Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
loopx-agent
left a comment
There was a problem hiding this comment.
Reviewer: model_agent; gpt-6.1-sol; OpenAI; runtime_reported; reasoning_effort=max.
动机
这项修改针对在 Windows 上运行 canary 和 issue-fix 校验的开发者:检查命令已经成功,但 Git 留下的只读对象无法删除,最终却收到失败结果,还保留了整个临时目录。预期改善是让这类正常检查成功收尾,同时真正无法完成清理时仍保留目录和原命令输出。
本次按完整 head d1062324ca2137233613c5c468c47dec12976c50 审查九个文件,没有继承作者自审结论。原始问题见 #6710;独立边界依据是 docs/development/testing-and-quality.md,固定版本 555a83df107c4711de49ee5451af5e34f4fff558 的 Smoke cleanup:清理不能跟随 namespace 或 locator 符号链接,失败须保留现场。当前仍有一个 P2:新的 Windows 预处理会先修改链接外文件的权限,再失败。这需要在合入前修复。本次不认证原生 Windows 的完整稳定性或持续运行,也不涉及操作员 restart、调度规则或安装升级。
改动思路
先让已有的 managed runtime 清理确认进程退出,再统一删除自有目录;仅 Windows 在删除前清除只读文件属性。这个公共文件系统 helper 放在现有 process_runtime 中合理,canary、issue-fix 和两个已有 smoke 复用它,各自仍保留现有失败/重试责任。没有必要新增能力、配置开关、TS 决策层或通用重试框架。Windows 上的属性处理属于平台 IO,保留 Python 是合适的。
但是文件条目的 islink 检查不足以保护根目录。os.walk(root) 会遍历作为起点传入的目录链接;随后才执行的 shutil.rmtree(root) 虽然拒绝这个链接,已经来不及阻止此前的 chmod。最小修复是在任何属性修改前拒绝根目录链接,并补充外部目标完全不变的测试。保留当前 helper 和调用关系即可,不需要扩大本 PR。
具体改动
loopx/extensions/process_runtime.py:新增_clear_windows_read_only_files和remove_owned_tree。前者扫描、跳过文件链接、按需加写权限;后者在 Windows 调用前者再执行rmtree。异常仍交回调用者,但目前遗漏根目录链接。loopx/canary/runner.py:_remove_fixture_root先退休 fixture runtime,再调用新 helper;_run_check保持清理失败时failed、保留目录和命令输出的行为。这里的失败结果并不能撤销已经发生的外部权限修改。loopx/capabilities/issue_fix/acceptance_loop.py和两个issue-fix-reviewer-*.pysmoke:替换原来的删除实现,沿用原有有界重试。没有改变候选评审或接受语义。- 三个测试文件覆盖只读成员、正常删除和清理不能确认时的失败结果;Windows PowerShell workflow 把这些测试真正纳入平台 lane。这修正了此前“POSIX 测试通过但 Windows lane 未运行新增回归”的覆盖缺口;我没有查询或派发 CI。
独立反例走真实 run_canary_smoke_check:子进程把其自建临时根替换成指向另一个合成目录的链接,外部目录只有一个 0444 普通文件。相同脚本在 base 和 head 上运行,只替换 process_runtime 的平台分支选择,其余 walk/stat/chmod/rmtree、子进程和 canary 结果均为真实调用。base 报清理失败且外部文件保持 0444;head 同样报失败,但文件已变成 0644。这是 Windows 分支的控制实验,宿主是 macOS,不能冒称原生 Windows 复现;它明确定位了新增副作用的因果路径。
对主干的风险
[P2] 在修改属性前拒绝根目录链接。 位置:loopx/extensions/process_runtime.py:253,调用入口在 remove_owned_tree:280–283。触发条件是清理开始时根目录已经成为目录符号链接。当前文件级防护不会检查起点,于是失败的清理仍会修改自有 namespace 之外的只读文件。canary 和 issue-fix 共用此 helper,所以不能依靠某一个调用者最终报错来证明隔离。请增加根链接负例,并断言目标目录及文件权限、内容都不变;保留当前普通文件/文件链接/正常只读成员测试。
验证结果:116 个相关 pytest 通过;Ruff、语义 inventory advisory 后的完整 semantic 检查、DCO 和 diff hygiene 通过;原生 premerge 的 6 个直接检查及 19 个选中检查通过。另有三组相同输入的真实 POSIX base/head 对照:普通文件成功、只读文件成功、命令退出 7;两侧都正确删除自有目录并保留原退出结果。上述绿色结果没有覆盖根链接反例,不能抵销该 P2。
原生 Windows 和 Windows junction 没有独立运行,仍是明确限制;我没有把 junction 当作已证明的新缺陷。初次本地反例命令因脚本路径写成相对路径而未启动,改用绝对路径后才形成上述 base/head 证据,这个启动错误不算产品失败。
我的整体评价
REQUEST_CHANGES。动机成立、复用方向和修改规模合适,当前边界应是“安全地清理自有 fixture 的只读成员”,而不是“能删掉目录即可”。Smoke cleanup 的隔离要求被根链接路径违反,需要先补最小 guard 和敏感回归,再重新核验普通 Windows 只读场景与失败现场保留。
相邻重构检查已做:统一这四个删除调用点有价值,保留各自重试/结果所有权;没有需要另建抽象或扩大迁移的理由。现有 CLI 与前端无需新增导航、复制参数或确认步骤;正常任务仍直接运行、读回结果,错误应准确且不修改外部目录。该评审不授予合并、生产清理或升级权限。
English verdict: REQUEST_CHANGES - d106232: the Windows pre-walk chmods files outside an owned fixture when its root is a directory symlink, before rmtree refuses it. Base/head real-runner counterfactual confirms the added side effect; 116 related tests and 6 direct/19 selected premerge checks pass. Reject the root link before chmod and add a no-external-mutation regression; native Windows qualification remains unverified.
| unlinks a read-only member of a writable directory. Links are skipped so | ||
| this walk cannot reach a target outside the owned tree. | ||
| """ | ||
| for directory, _subdirectories, files in os.walk(root): |
There was a problem hiding this comment.
[P2] 在 walk 前拒绝根目录链接。这里只跳过 files 中的链接,但 os.walk 会跟随作为 root 传入的目录链接:若 smoke 在退出前把自建临时根替换成指向外部目录的链接,新增 Windows 分支先把目标文件从只读改成可写,随后 rmtree 才拒绝链接。真实 canary runner 的同输入 base/head 控制实验中,两者均报 cleanup failed,但 base 保持外部文件0444,head改成0644;仅平台分支选择被替换,其余文件系统和子进程真实,宿主macOS,未冒称原生Windows。请在任何 chmod 前拒绝根链接,并补断言外部文件权限及内容不变的负例,保留普通只读成员可清理的覆盖。
There was a problem hiding this comment.
已按 P2 修复,新 head 为 5c7f2d9c1(已 rebase 到 f9bdae675)。
修复
remove_owned_tree在任何属性修改前拒绝根链接:os.path.islink覆盖目录 symlink;Windows 额外通过getattr(os.lstat(target), "st_file_attributes", 0) & FILE_ATTRIBUTE_REPARSE_POINT识别目录 reparse point(junction/mount point,兼容 Python 3.11,os.path.isjunction3.12 才提供)。拒绝时抛OSError("refusing to remove linked tree root: ..."),调用方原有的 fail-closed/保留现场语义不变。_clear_windows_read_only_files在 walk 中同步剪掉链接目录,嵌套 junction 也不会被改权限。
负例(断言外部目录文件内容与只读属性均不变)
test_remove_owned_tree_refuses_a_junction_root_without_touching_its_target:原生 Windows 上用无特权mklink /J构造根 junction,目标文件0444+b"blob"在调用后保持不变;该用例在修复前的 helper 上失败。test_remove_owned_tree_refuses_a_symlinked_root_without_touching_its_target:目录 symlink 的同一契约;本机无 symlink 权限时 skip,Windows lane 的 junction 用例是判别器。- 原有普通只读成员可清理的用例保留。
原生 Windows 对照(本机 Windows 11 / Python 3.14):修复前根 junction 下目标文件 0444 → 0666,随后抛 Cannot call rmtree on a symbolic link;修复后调用直接抛 refusing to remove linked tree root,文件保持 0444 且内容不变。
验证
- 三个套件 11 passed / 27 skipped(
tests/extensions/test_process_runtime.py8 passed:junction 用例执行,symlink 用例因本机无权限 skip);加上scripts/ci/test_windows_lane_coverage.py后 13 passed / 27 skipped / 12 subtests。 ruff check通过;rebase 后已重跑。python -m mypy --platform linux:17 files 通过。首版直接访问st_file_attributes会在 Linux 类型检查下报错(上一 head 的kernel-static-checks失败原因),现已改为getattr读取。- PR body 已更新:criteria 增加“teardown 不改变自有树外权限”,并更新 Validation 与限制说明。
附带说明:上一 head 的 test-shard 失败与本 diff 无关——test_top_level_module_budget 是 main 侧 loopx/workflow_skill_install.py 使顶层模块变为 146、而 pinned budget 仍为 145 的既有不一致(本 head 上同样复现);另两处断言在本 head 本地通过,且所在文件未被本 PR 改动。
请在新 head 上重新审阅。
2614c84 to
3b91cf8
Compare
…ad-only git objects On native Windows the canary smoke runner could not remove its own per-check fixture root whenever the smoke created a git repository inside it: git marks every loose object and pack read-only, and Windows refuses to unlink a read-only file, so `shutil.rmtree` raised `PermissionError` [WinError 5]. The runner then recorded the check as failed although its command had exited 0, retained the tree, and reported the cleanup error as the check's own reason, so a contributor could not tell a real regression from harness noise. Owned tree removal now goes through `extensions.process_runtime.remove_owned_tree`, which clears the Windows read-only attribute of the files in the tree before removing it. POSIX behaviour is unchanged: there a read-only member of a writable directory never blocks unlink, so no extra code runs. A removal that still cannot complete raises, so a caller that must retain the tree for diagnosis keeps observing it. The same host behaviour aborted the issue-fix packet's temporary git workspace teardown from its `finally` block after five pointless retries, and in two public reviewer smokes it masked a real failure behind the cleanup error; those call sites now use the same owner. Refs loopx-project#6710 Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
The canary-runner and issue-fix teardown regressions fail on native Windows with `PermissionError` [WinError 5] when the fix is absent, and pass on POSIX either way because a read-only member never blocks unlink there; the pre-fix behaviour was reproduced by substituting a plain `shutil.rmtree` at the two call sites. Refs loopx-project#6710 Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
The new regression suites pass on POSIX with or without the fix, because unlink there ignores the read-only bit, and the curated `windows-powershell` lane ran none of them, so the native-Windows behaviour they encode had no guard on the host where it reproduces. Point that lane's qualification at them. Refs loopx-project#6710 Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
3b91cf8 to
324881a
Compare
os.walk starts at a root that is itself a directory symlink or Windows junction, so the Windows pre-pass could clear the read-only attribute of a file in the link target before shutil.rmtree refused the link. Refuse a linked root first, and skip linked directories inside the walk, so teardown never changes permissions outside the owned tree. Cover the rejection with a no-external-mutation regression for both a directory symlink and a native Windows junction. Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
324881a to
5c7f2d9
Compare
loopx-agent
left a comment
There was a problem hiding this comment.
Reviewer: model_agent; gpt-6.1-sol; OpenAI; runtime_reported; reasoning_effort=max
动机
在 Windows 上运行 canary 或 issue-fix 校验的开发者,需要检查结果准确,并且清理只作用于自己的临时目录。
以前只读 Git 对象可能使成功命令在清理时被判失败;现在统一处理只读属性并提前拒绝根链接,但普通本地 Git 克隆的硬链接仍会把源仓库对象改成可写。
根目录链接反例在当前 head 已保持外部文件不变;新的真实 runner 对照中,两边都成功清理克隆目录,只有 head 改变了目录外三个源对象的权限。
本次不认证原生 Windows 的完整稳定性、junction 或持续运行,不涉及操作员 restart、生产清理或安装升级。
仍需保护共享硬链接的源文件,补充普通本地克隆的无外部属性修改回归,然后复评新 head。
改动思路
平台文件系统处理复用现有 process_runtime,调用者保留重试和失败结果;修复硬链接隔离即可,无需新增能力或重试框架。
本 PR 的边界是安全地清理自有 fixture 的只读成员,外部文件的属性与内容必须保持不变。
同一准确head的源码关系如下;这是解释表,不替代执行证据:
| 清理对象 | 原路径 | 当前结果 |
|---|---|---|
| 自有普通只读成员 | Windows的plain rmtree可能失败 | 先处理属性,再删除;普通POSIX正反例保持原命令结果 |
| 根目录symlink | 上轮先walk外部目标再失败 | 本head提前拒绝,外部0444与内容不变 |
| 本地clone中的普通硬链接对象 | 删除克隆链接不修改源对象属性 | 本head把共享对象chmod为可写,源仓库也改变,最终却报告通过 |
硬链接是同一文件的另一个目录项;它不是symlink或junction。因此根链接guard修复成立,但不能证明每一个普通文件都由临时目录独占。平台IO留在Python是合适的;判定自有资源的边界不能由一个成功清理结果反推。
具体改动
完整head 5c7f2d9c13db3d7b5a10d84988cd9bb87b60340a,实际base f9bdae67596c185cebc81dc555dd6a3e0242e9c5,9路径、+328/-10。本轮先遇到2614→324881aa4,再遇到当前5c7f2d9c1;每次重新取得native exact-head plan并读取frame增量。最后一处源码差异是通过getattr读取Windows专有属性,已重新执行公共runner反例和当前head检查,没有把旧head声明当作新执行。
独立依据是原始问题 #6710 及改动前 docs/development/testing-and-quality.md 的 Smoke cleanup:清理不能跟随namespace/locator链接,无法确认时保留现场和原命令输出;现有隔离合同不授予目录外文件效果。按这些criteria:原问题 Expected behavior 的普通只读成员/失败报告路径已覆盖,根链接无外部修改已验证,共享硬链接的无外部属性修改 not_met。不是用本PR新docstring定义正确性。
关键代码讲解
_is_link_or_junction(loopx/extensions/process_runtime.py:246)先检测symlink;Windows再由lstat的directory/reparse属性拒绝junction/mount。remove_owned_tree:318–323在任何属性处理前检查根。这补上上一轮的真实root-link缺口,并保留caller失败/现场留存。_clear_windows_read_only_files:283–298剪掉目录链接,跳过文件链接,然后对其余只读文件调用os.chmod。问题位于294–296:普通硬链接会通过这些检查;chmod改变的是共享文件,而不只是这一个目录项。_remove_fixture_root(loopx/canary/runner.py:290)先退休自有runtime,再调用公共删除helper,_run_check仍在真正清理失败时保留目录、命令returncode和输出。新硬链接反例没有抛异常,故这个error分支看不见已经发生的越界属性修改。_remove_temporary_git_workspace(loopx/capabilities/issue_fix/acceptance_loop.py:259)以及两个现有reviewer smoke复用helper,各自保留既有有界重试。统一文件系统规则有价值,不能靠重复重试撤销外部chmod。
三个测试文件覆盖read-only成员、cleanup fail-closed及root symlink/junction;Windows PowerShell lane真实包含它们,避免只在POSIX上验证一个不具平台区分力的成功结果。没有新增共享状态、CLI、持久化字段、额度或第二个TS/Python决策owner;其它未使用这个helper的旧清理点不因形状相似而被强制纳入本次。
对主干的风险
[P2] 修改只读属性前保护共享硬链接。 loopx/extensions/process_runtime.py:294–296。触发不需要竞态或恶意目录替换:smoke对子目录外的本地合成Git仓库执行普通 git clone --local --no-checkout,Git在克隆目录内创建硬链接对象。新清理把克隆对象加写权限时,源仓库的同一对象也被修改。成功结果不能授权这个外部效果。
独立控制实验走真实 run_canary_smoke_check、真实Git子进程和walk/stat/chmod/rmtree:两端相同输入,清理前源/克隆对象inode相同且link count均为2;base与head均returncode0、status passed、删除自有root,但只有head把目录外三个源对象从0444改为0644,原内容和inode不变。宿主是macOS,只选择helper的Windows分支;当前源码已经用getattr兼容该属性,因此本次lstat也保持真实调用,其余IO真实。这是分支因果实验,不是原生Windows或NTFS证明,仍精确定位新增副作用,不能用native平台尚未执行把已观测路径标绿。
最小修复是对可能共享的文件保持fail-closed,或先在自有路径解除共享再处理属性;不能把“不是symlink”当成独占。请补真实本地clone或硬链接fixture,断言源文件内容和属性不变,并保留根链接拒绝、普通只读删除、失败现场、命令失败结果与Windows lane。新head至少重跑 uv run --extra test python -m pytest -q tests/canary/test_smoke_fixture_cleanup.py tests/extensions/test_process_runtime.py tests/capabilities/test_issue_fix_temporary_workspace.py,并通过公共runner复现该输入。
独立本地验证:119 passed、1 skipped、12 subtests;唯一skip为native Windows junction。Ruff覆盖所有改动Python、advisory后full semantic、9路径公开扫描、DCO/diff/bilingual通过;标准premerge为6 direct+19 selected全过,真实issue-fix e2e exit0。另有普通文件成功、只读文件成功、只读文件命令exit7的三组真实POSIX base/head对照,均正确清理并保留原命令状态。所有绿色项仍遗漏这个新硬链接反例。根链接当前已拒绝并保持外部0444,未重复报告已修复的旧finding;未查询、轮询或派发CI。
我的整体评价
REQUEST_CHANGES — 5c7f2d9c13db3d7b5a10d84988cd9bb87b60340a,保留一项上述P2。原始用户价值和共享helper归属合理,necessary/proportionate,architecture retain;long_horizon 为 regression,因为重复验证可无声修改共享源对象,minimum repair是补硬链接隔离;user_experience 为 preserved,仅指原命令/导航/失败读回未增加准备或确认,不能抵消外部副作用。
未来改动的简化检查已应用:合并四处平台删除规则,并保留不同caller的重试/结果所有权;不需要泛化资源管理器或无关语言迁移。协议名 remove_owned_tree 与所需语义相称,当前实现未守住该名称要求的ownership边界;OS条件是平台行为,不是默认关闭的可选能力。语义词表保持local_only,未新增共享状态枚举;现有独立隔离义务是必须守住的invariant,不是指导建议。原生Windows读写属性、junction、长期稳定性仍未独立qualified,修复后需准确head复评;本评审不授予merge或真实生产清理权限。
English verdict: REQUEST_CHANGES — 5c7f2d9. Root-link rejection is fixed, but the Windows chmod pre-pass still modifies shared hard-linked Git objects outside the owned fixture after a normal local clone, while the real runner reports success. Immutable-base/current-head branch-controlled comparison confirms the added effect;119 tests and6 direct/19 selected checks pass but miss it. Protect shared files and add a no-external-mutation regression; native Windows remains independently unverified.
| try: | ||
| mode = os.stat(target).st_mode | ||
| if not mode & stat.S_IWRITE: | ||
| os.chmod(target, mode | stat.S_IWRITE) |
There was a problem hiding this comment.
[P2] chmod 前保护共享硬链接。普通 git clone --local --no-checkout 可让临时克隆与目录外源仓库共享只读对象;它们不是 symlink/junction,会走到这里的 chmod。独立真实 canary runner 同输入 base/head 对照:清理前 source/clone inode相同且 nlink=2,两边均报告passed并删除自有root,只有head把三个外部源对象0444改为0644,内容与inode不变。宿主macOS,仅选择Windows分支;lstat、Git、子进程和文件系统操作均为真实调用,非nativeWindows证明。请在改属性前拒绝或安全隔离可能共享的文件,补本地clone/硬链接无外部属性修改负例,并保留普通只读删除、根链接拒绝和失败现场回归。
A git clone --local hard-links loose objects, and Windows keeps the read-only attribute on the shared file rather than on one directory entry. The Windows pre-pass therefore cleared the source repository object too, while the teardown still reported success. Leave every file with more than one link untouched so rmtree fails closed and the caller retains the fixture instead of mutating an object outside it. Cover the fixture entry and the real canary runner with a hard-link regression that asserts the external object keeps its bytes and read-only attribute. Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
|
已在 head 修复
负例
原生 Windows 对照(本机 Windows 11 / Python 3.14):修复前 验证
权衡(已写入 PR body):包含只读硬链接对象的 fixture 现在会 fail-closed(报告失败并保留现场),这是“外部文件属性与内容不变”要求下的边界。现有 issue-fix repository fixture 用 PR body 已同步更新(criteria、review 记录、Validation 与限制)。请复评新 head。 |
loopx-agent
left a comment
There was a problem hiding this comment.
Reviewer: model_agent; gpt-6.1-sol; OpenAI; runtime_reported; reasoning_effort=max
动机
在 Windows 上运行 canary 或 issue-fix 校验的开发者,需要成功命令得到准确结果,同时清理只影响自己的临时目录。
旧路径遇到 Git 的只读对象会清理失败;上一版修复又会把普通本地克隆共享的源对象改为可写,尽管 runner 报告成功。
当前补丁保留普通只读成员清理,并在修改属性前跳过共享硬链接;真实 runner 对照中,外部源对象保持原字节和只读权限。
本次没有独立认证原生 Windows、NTFS junction、持续运行或整机测试健康,也不涉及生产目录清理、安装升级或合并。
改动思路
这轮修复成立。文件硬链接是同一文件的另一个目录项;检查它不是 symlink,还不能证明临时目录独占该文件。st_nlink > 1 时不清除共享属性,保留 rmtree 与 caller 的失败报告。Windows 无法删除的只读共享链接因此保留现场;POSIX 可以直接删除自己的目录项,源文件也不变。
平台文件属性规则统一放在已有 process_runtime,调用者继续拥有进程退休、重试和检查结果,不新增资源管理框架。
当前 PR 边界是安全清理自建 fixture 的普通只读成员,并拒绝会改动外部共享文件的属性处理;共享只读对象在 Windows 上允许明确失败并保留现场。
这是必要的安全权衡:做不到安全删除时应报告失败,不能为了把检查标绿而修改其他仓库。直接忽略 cleanup error 会隐藏泄漏;只在一个 smoke 内修复会留下 issue-fix 等相同入口。共用删除规则、保留 caller 各自重试已经是较小方案。这里的比较表只是源码解释,执行证据见下文。
| 输入 | 旧问题或上一版 | 当前边界 |
|---|---|---|
| 自有普通只读对象 | Windows unlink 失败 | 单链接文件可先清属性再删除 |
| 根目录 symlink/junction | 曾先改外部目标再拒绝 | 在 walk/chmod 前拒绝 |
| 本地 clone 的共享硬链接 | 上一版连同源仓库一起 chmod | 不改共享属性;不能安全删除时失败并留存 |
| 普通 POSIX 成功/命令失败 | 原命令结果与清理分别处理 | 保留原 returncode,自己的 namespace 正常删除 |
具体改动
完整 head 803c0e242fce52aeaf0e97f087a7c2f39a1e404a,实际 base f9bdae67596c185cebc81dc555dd6a3e0242e9c5,9 个路径、+450/-10;本轮同时审查完整 PR 与上一评审 5c7f2d9c13db3d7b5a10d84988cd9bb87b60340a 到当前 head 的增量,没有继承旧结论。
独立依据先于 diff 阅读:原问题 #6710 的 Expected behavior,以及改动前 docs/development/testing-and-quality.md 在上述完整 base 的 Smoke cleanup:自有 namespace、禁止跟随链接、无法确认清理时保留 fixture 和原命令输出;不认证原生间歇故障或持续运行。原问题的普通只读删除路径保留;共享文件的无外部修改由本轮反例验证,不能用新 docstring 自证正确。
关键符号与所有改动面:
remove_owned_tree(loopx/extensions/process_runtime.py:311)在任何属性处理前检查根链接,Windows 才进入_clear_windows_read_only_files:274,最后仍调用真实shutil.rmtree。_is_link_or_junction:247兼容 Python 3.11:动态读取目录 reparse 属性,避免依赖 3.12 新 API。- 属性遍历在
process_runtime.py:297读取真实stat,跳过st_nlink > 1的文件;目录链接会先从 walk 中剪掉。它不扩展文件所有权,也没有复制或改写外部源对象。 _remove_fixture_root(loopx/canary/runner.py:290)先退休该 fixture 的 runtime,再删除目录。_run_check保留失败原因、命令输出和原 returncode;不能确认删除时不会二次强删。issue_fix/acceptance_loop.py::_remove_temporary_git_workspace和两个现有 reviewer smoke 改用同一 helper,保留原来的五次/十次有界重试。没有把各 caller 的业务结果合并成新的状态 owner。- 三个回归文件覆盖普通只读成员、无法确认清理、根链接/junction 和共享硬链接;
.github/workflows/python-tests.yml的 Windows PowerShell lane 实际列入这三套测试。工作流阅读不代表这次独立执行过 Windows,也没有查询 CI。
独立控制实验采用同一合成输入,走真实 run_canary_smoke_check、Git 子进程、walk/stat/chmod/rmtree 和结果读回:普通 git clone --local --no-checkout 使 fixture 与外部源对象共享 inode,清理前 link count=2。base、上一版和当前版都完成 POSIX 删除;只有上一版把外部三个只读对象从0444改为0644,当前版保持0444、内容及 inode 不变。另测根链接:当前版拒绝且目标保持0444。宿主是 macOS,只有 helper 平台分支选择为 Windows;不是原生 Windows 证明。 不能据此声称 Windows 上共享对象一定已经删除;预期恰好是保持属性、必要时失败。
另有 base/上一版/head 三组真实 POSIX 对照,每组包含普通成员成功、只读成员成功、只读成员命令 exit7;九条路径都正确清理并保留命令结果。原先根链接 finding 没有重复保留,共享硬链接 finding 也已由本轮相同输入反证修复。
对主干的风险
没有发现当前范围内的阻断项。最主要的剩余限制是 Windows 的真实文件属性、junction 和长期运行仍未由我独立执行;作者提供的原生 Windows 报告保留为作者证据,不能当作本轮执行。当前 source guard、真实共享 inode 反例、原有进程/结果处理与平台 lane 接线共同支持这个有界修复。
独立本地验证:116 passed、1 skipped、12 subtests;skip 是原生 Windows junction。Ruff 覆盖所有改动 Python,advisory 后完整 semantic、公开边界扫描、五个提交的 DCO 与 diff hygiene 通过。标准 premerge 的6个直接检查及19个选中检查全部通过,包含真实公共 canary/issue-fix 入口与 pytest facade;未查询、派发或等待 CI。公开扫描有两个隔离目录中缺本地 registry 的警告,0 error,不能称零警告。起初一个测试命令写错不存在的路径,未运行测试;使用实际文件后才得到上述完整结果。
删除规则在 Windows 自动生效,属于修复既有平台行为,不是新可选策略;原 POSIX 分支保持。只读共享文件留存是公开说明的失败边界,而不是“成功清理”的无条件保证。不要据这次通过结论扩大到其他 shutil.rmtree 入口或解除进程占用问题,它们有不同 owner。
我的整体评价
APPROVE — 当前准确 head 的安全修复和全部9路径已重新核验。原来的根链接和硬链接问题都不再构成本 head 的 blocker;Windows 平台 qualification 和全机长期健康仍单独保留。Smoke cleanup 与 Expected behavior 的本次适用要求满足,未把源码通过升级成完整平台稳定性。
相邻的未来改动检查已应用:统一四个调用点的文件属性规则,保留进程退休、重试及失败读回各自 owner;当前没有需要新增抽象、状态机或 TS 重写的同域修复。普通 CLI 校验依旧直接运行并读回,已有意图与授权下零新增确认轮次;错误保持可见。没有新前端操作或 Lark 配置需要补界面,也没有增加持久化状态、授权、额度或调度规则。此通过结论不授予 merge、dismissal 或生产清理权限。
English verdict: APPROVE - 803c0e2. The shared-hard-link mutation is fixed: a real Git/canary comparison preserves external object bytes and0444 modes at this head, while the previous head changed them.116 tests and6 direct/19 selected checks pass; native Windows/junction qualification remains independently unverified, and unsafe shared read-only cleanup may fail closed.
Goal And Delivered Outcome
Outcome basis / optional anchor: Closes [Bug]: native Windows cannot delete a smoke fixture that contains a git object store, so the canary premerge gate reports a passing check as failed and retains the fixture #6710 — a reproduced defect with the raw before
evidence.
Goal/source and gap: on native Windows the canary smoke runner could not remove a
per-check fixture root that contained a git object store. Git marks every loose object and
pack read-only, and Windows refuses to unlink a read-only file, so
shutil.rmtreeraisedPermissionError[WinError 5]; the runner then recorded the check as failed although itscommand had exited 0, retained the tree, and put the cleanup error in the check's
reason. The pre-merge gate this repository requires contributors to run(
loopx canary premerge --from-git-diff) attaches thecanary-runnerrisk profile to everycontrol_planepath, so on Windows it reported a failure the diff did not cause for everycontrol-plane diff, and the same host behaviour aborted the shipped issue-fix
repo-branch-fixturecommand, two public reviewer smokes, and the documented pytest facadein
tests/test_smoke_suite.py.Observable before → after, with the validation row that proves it: the same single-check
invocation of the canary runner reported
status: failed/ok: false/returncode: 0with reason
fixture cleanup failed; retained <TEMP>\loopx-smoke-…before, and reportsstatus: passed/ok: true/failure_count: 0after. See thereal_entrypointandregression_parityrows below.Issue/task and intended base: Closes [Bug]: native Windows cannot delete a smoke fixture that contains a git object store, so the canary premerge gate reports a passing check as failed and retains the fixture #6710; intended base
main.Author Declaration
model_agent— an AI agent running in DeepSeek Harness (modeldeepseek-v4.1-flash).AI disclosure: this change was drafted with AI assistance at my request, under my direction
and review. I set the constraints (one host behaviour, both owners it reaches, no unrelated
cleanup or refactor) and reviewed the diff, the failing-before evidence and the check output
before submission.
Implemented against
teardown; the basis is the reproduced defect and its acceptance criteria in
#6710, against base revision
fdf03ec61.loopx/canary/runner.py::_remove_fixture_roottests/canary/test_smoke_fixture_cleanup.py::test_a_passing_check_survives_a_read_only_fixture_member;real_entrypointrowloopx/extensions/process_runtime.py::remove_owned_treeloopx/canary/runner.pycleanup-error pathtests/canary/test_smoke_fixture_cleanup.py::test_the_runner_still_reports_cleanup_that_cannot_be_confirmedloopx/capabilities/issue_fix/acceptance_loop.py::_remove_temporary_git_workspacetests/capabilities/test_issue_fix_temporary_workspace.py;python examples/issue-fix-workflow-e2e-smoke.pyremove_owned_treeacts only onos.name == "nt"for attributes; a linked root is refused on every hostremove_owned_treelinked-root guard;_clear_windows_read_only_filesskips linked directoriestests/extensions/test_process_runtime.pysymlink and native-Windows junction no-mutation regressions_clear_windows_read_only_filesleaves any file withst_nlink > 1untouched and letsrmtreefail closedtests/extensions/test_process_runtime.pyhard-link regression;tests/canary/test_smoke_fixture_cleanup.pyreal-runner regressionexamples/issue-fix-reviewer-recommendation-smoke.py,examples/issue-fix-reviewer-request-smoke.pyloopx canary smoke-suite --suite full-public --script …sweep rowSelf-check before submission: read the changed call sites and the pre-merge classification
that attaches
canary-runnerto control-plane diffs; ran the real entrypoints before andafter; confirmed the three remaining
tests/canaryfailures and the fourWinError 32smoke failures are present on the untouched base revision (evidence below) and deliberately
left them out of scope.
Placement rationale for the new owner:
remove_owned_treesits inloopx/extensions/process_runtime.pybesideterminate_process_tree, the module thatalready owns teardown of an owned, previously isolated host resource and is imported by
loopx/canary/runner.py,loopx/control_plane/runtime/validation_command.py,loopx/chat_agent.pyandloopx/zcode_goal_mode/diagnostics.py. A new top-level modulewould spend part of the pinned top-level module budget (see [Bug]: the pinned top-level module budget is already exceeded on main (150 measured against 147) #5685) for one stdlib-only
function, and a home under
loopx/canary/would make a capability import the gate it isvalidated by.
Scope And Continuation
Completed scope and remaining work: complete within this scope. Named items deliberately not
touched:
WinError 32because a livehelper still holds the directory; that is a different root cause and is already reported
as [Bug]: Windows background helpers (detached usage sender, effect-runtime host) pin the caller's directory so it cannot be renamed or removed (WinError 32) #6642 (PR fix(runtime): stop long-lived helpers pinning the caller's directory on Windows #6652).
loopx/capabilities/integration_branch/core.pyremoves a git worktree root withshutil.rmtree(..., ignore_errors=True), so on Windows it silently leaves the treebehind. It is a leak rather than a failure and no shipped gate reads it, so it is left
for a separate change.
examples/issue-fix-reviewer-request-smoke.pyhas an unrelated pre-existing Windowsfailure (
trusted_clock["secondary_notification_status"]); this PR only stops theteardown error from masking it.
bash-dependent installer checks,unprivileged symlink creation, extension doctor readiness,
loopx-update-smoke) remainand are outside this defect.
Slice boundary / successor: N/A — the reproduced defect is closed; the remaining items above
have their own owner or are named as separate work.
Independent review, and what it changed: an independent adversarial review of revision
97756955e(value, scope, logic and edge cases, abstraction, test sufficiency, checkplausibility) returned APPROVE WITH NOTES and reproduced the before/after verdict, the two
smoke entrypoints, the facade and the pre-existing failures on the untouched base. Its two
findings are fixed here: (1) the three regression suites ran on no hosted Windows lane, so
the guard was inert where the bug reproduces —
.github/workflows/python-tests.ymlnowqualifies them on
windows-latest; (2) one canary test monkeypatched a symbol this PRintroduces and therefore tested nothing pre-fix — it is now a fail-closed contract test over
the teardown seam (
remove_owned_tree), and the tautologicalshutil.rmtreemock test wasdropped. The reviewer's residual nits are deliberately not applied: the pre-existing
ten-attempt loops in the two example smokes and the five-attempt loop in the issue-fix
teardown keep their transient-race handling, and each attempt now re-walks the tree, which is
bounded by the size of the owned fixture.
Second independent review, and what it changed: a review of
d1062324creturnedCHANGES_REQUESTED with one P2 —
os.walkstarts at a root that is itself a directory symlinkor Windows junction, so the Windows attribute pass cleared read-only bits in the link target
before
shutil.rmtreerefused the link (base/head counterfactual through the real runner:target file
0444throughout on base versus0444→0666on head). Fixed at this head:remove_owned_treerefuses a linked or junction root before any attribute change, and theWindows walk now prunes linked directories so a nested junction is not rewritten either. New
regressions cover a directory symlink and a native Windows junction and assert the external
file keeps both its bytes and its read-only attribute. The reviewer's native-Windows and
junction qualification limits are addressed for junction roots by running that regression on
the shipped
windows-powershelllane; a directory-symlink case still skips on hosts withoutsymlink privilege, where the junction case is the discriminator.
Third independent review, and what it changed: a review of
5c7f2d9c1returnedCHANGES_REQUESTED with one P2 — a normal local git clone hard-links loose objects, so the
clone object and the source object are one file with one read-only attribute; the Windows
attribute pass cleared that shared bit outside the fixture while teardown still reported
success. Fixed at this head:
_clear_windows_read_only_filesleaves every file withst_nlink > 1untouched, soshutil.rmtreefails closed and the caller retains the fixtureinstead of changing another owner's object. Regressions cover the helper with a hard-link
fixture and the real canary runner (command exits 0, cleanup failure is reported, the fixture
is retained, and the external object keeps
0444and its bytes). Native-Windowscounterfactual: the pre-fix helper returned success and moved the source object
0444→0666; the fixed helper raisesPermissionErrorand leaves it0444. Disclosed tradeoff:a fixture that leaves read-only hard-linked objects behind now reports cleanup failure and is
retained; that is the fail-closed boundary. The shipped issue-fix repository fixture creates
its own objects with
git init(link count 1), so its passing path is unchanged.Validation
803c0e242(basef9bdae675). Independent adversarial reviews wereperformed at
97756955e,d1062324cand5c7f2d9c1; every finding is fixed in this head andnamed under Scope And Continuation.
staticpassedpython -m ruff checkover every touched file:All checks passed!.scripts/generate_semantic_inventory.py --changed-from HEAD: no supported new vocabulary carriers (the change introduces no enum, literal or closed set).unitpassedtests/canary/test_smoke_fixture_cleanup.py3 passed;tests/capabilities/test_issue_fix_temporary_workspace.py1 passed;tests/extensions/test_process_runtime.py9 passed / 27 skipped (the 26 existing POSIX-only process-group regressions plus the directory-symlink case, which needs symlink privilege on this host; the native junction and hard-link cases run and pass here).regression_paritypassedremove_owned_treeon native Windows: pre-fix the call returned success and the source object went0444→0666; at this head the call raisesPermissionError, the source object stays0444with its bytes unchanged, and the fixture is retained.test_remove_owned_tree_never_clears_a_shared_hard_link_attributefails on the pre-fix helper and passes here;test_a_shared_hard_link_is_not_rewritten_outside_the_fixturepins the same input throughrun_canary_smoke_check(returncode 0,ok: false, cleanup reason, retained root).regression_paritypassed.git/objects-shaped file, run through the realremove_owned_treeon native Windows: pre-fix the file went0444→0666and the helper then raisedCannot call rmtree on a symbolic link; at this head the same call raisesrefusing to remove linked tree rootbefore the walk and the file stays0444with its bytes unchanged.test_remove_owned_tree_refuses_a_junction_root_without_touching_its_targettherefore fails on the pre-fix helper and passes here; the symlink case pins the same contract cross-host.regression_paritypassedshutil.rmtreeat the two call sites (the pre-fix behaviour) on native Windows makestest_a_passing_check_survives_a_read_only_fixture_memberandtest_temporary_git_workspace_teardown_removes_read_only_objectsfail withPermissionError[WinError 5] on a.git/objectspath, and makes the pytest-facade check fail withassert False is True. With the owner they pass. On POSIX the same tests pass either way, because unlink ignores the read-only bit there; the Windows host is the discriminator.real_entrypointpassedloopx canary smoke-suite --suite full-public --script examples/canary/catalog-planner-smoke.py: beforestatus failed,ok false,returncode 0, reasonfixture cleanup failed … [WinError 5] … .git\objects\…; afterstatus passed,ok true,failure_count 0,stdout_tailcatalog-canary-planner-smoke ok.real_entrypointpassedpython examples/issue-fix-workflow-e2e-smoke.py: beforePermissionError[WinError 5] fromacceptance_loop._remove_temporary_git_workspace; afterissue-fix-workflow-e2e-smoke: ok, exit 0.real_entrypointpassedpython -m pytest tests/test_smoke_suite.py --loopx-smoke-script examples/canary/catalog-planner-smoke.py: before 1 failed / 1 passed withassert False is True; after 2 passed.integrationpassedfull-publicchecks that invoke a git object write (loopx canary smoke-suite --suite full-public --jobs 4): failures 20 → 18; checks whose failure mentioned read-only unlink 4 → 0; verdicts changed failure → pass:catalog-planner-smokeandissue-fix-reviewer-recommendation-smoke; no check changed pass → failure. Two checks (issue-fix-acceptance-loop-smoke,issue-fix-reviewer-request-smoke) stay failing for their own, unrelated assertion once the teardown error stops masking it.manualfailedloopx canary premerge --from-git-diff --git-diff-base f9bdae675on this 9-file diff: 6 direct checks 0 failures (diff hygiene, RFC bilingual,py_compile, module ceiling);risk_profile_run8 executed / 0 failures;boundary_run1 executed / 0 failures;catalog_run8 of 10 passed. The two catalog failures are environment-bound and pre-existing on this host:cli-output-budget-regression-smokeexceeds the 120 s per-check budget (direct run: 202 s, then the knownPermissionError[WinError 32] on a temp root held by a live helper, tracked by #6642 / PR #6652), andissue-fix-json-input-boundary-smokehits a GBK console decode while capturing a UTF-8 child, unrelated to teardown. Aggregateok: falsefor those two; neither touches code changed here. The before run (fix absent, scratch control-plane diff) failed the same section with 3 failures, one of which wascatalog-planner-smokereportingreturncode 0as failed with the read-only reason.staticpassedpython -m mypy --platform linux:Success: no issues found in 17 source files(the Windows junction attribute is read withgetattr, so the cross-platformstat_resulttype is not widened).ruff checkover every touched file:All checks passed!; the semantic inventory advisory reports no supported new vocabulary carriers..github/workflows/python-tests.ymladds onewindows-powershellstep that runs the three regression files onwindows-latest, andscripts/ci/test_windows_lane_coverage.pystill passes (2 passed, 12 subtests). Without that step the new suites pass trivially on Ubuntu and never run on the host where the defect reproduces.unitfailedtests/canaryon this Windows host: 3 pre-existing failures (test_example_symlink_cannot_escape_selected_checkout,test_telemetry_isolation.py::test_update_smoke_minimal_environment_keeps_opt_out,test_telemetry_isolation.py::test_smokes_isolate_routes_and_home_in_serial_and_parallel). Proof they are not from this diff: the same 3 tests fail the same way on the untouched base revisionfdf03ec61in a clean worktree (unprivileged symlink creation, a POSIX-HOMEassumption inPath.home(), and the unrelatedloopx-update-smokeplan assertion).entrypoint runs on the host where the defect reproduces. Not covered by construction: none of
the read-only-member tests can discriminate on POSIX, where unlink ignores the read-only bit,
so the native-Windows lane is the discriminator and this PR adds the three suites to its
qualification step. The linked-root guard is covered on POSIX by the directory-symlink case,
and discriminatingly on Windows by the junction case, which needs no symlink privilege. The
shared-hard-link guard is covered twice: the helper-level hard-link regression and the
real-runner case, both asserting the external object keeps
0444and its bytes. Itslimitation is deliberate: a fixture that retains read-only hard-linked objects fails closed and
is kept rather than silently rewriting the source. CI on head
5c7f2d9c1passedkernel-static-checks(includingmypy),windows-powershell,typescript-core,dashboard-browser,stage2c, bothbuildjobs and the other lanes; itstest-shardjobswere still pending when this head replaced it, and they are rerunning here. The earlier
d1062324cshard failures weretests/architecture/test_top_level_module_budget.py(amain-side budget mismatch:
loopx/workflow_skill_install.pyis the 146th top-level modulewhile the pinned budget is 145, which reproduces on this head without any diff in that path)
plus two assertions in
tests/test_managed_effect_runtime_fixture.pyandtests/presentation/test_codex_attached_conversation.pythat pass locally on this head andlive in files this diff does not change. This head is rebased onto
f9bdae675, so CI rerunson the exact requested head.
Frontend / Visual Evidence
surface changed.
Type of Change
LoopX Area
Primary area: Build, packaging, installer, or CI (the canary pre-merge/smoke-suite gate); the
shared teardown owner and the issue-fix call site are why Host or runtime integration is also
marked.
Technical Direction
no roadmap S/G/R, milestone card or RFC section is claimed.
Shared-authority RFC fixture impact
contract is touched.
Boundary Checklist
.loopx/,.codex/goals/, and liveACTIVE_GOAL_STATE.md). Absolute host paths in the quoted evidence are shown as<TEMP>.python3 scripts/check_rfc_bilingual.py --base origin/main --head HEADpasses, or no paired RFC changed.none.Signed-off-bytrailer (git commit -s).