Skip to content

fix(turn-driver): ask the host for its no-follow flags - #8083

Merged
huangruiteng merged 5 commits into
loopx-project:mainfrom
gcl-coder:fix/portable-native-trace-flags
Nov 5, 2026
Merged

huangruiteng merged 5 commits into
loopx-project:mainfrom
gcl-coder:fix/portable-native-trace-flags

Conversation

@gcl-coder

Copy link
Copy Markdown
Contributor

问题

os.O_NOFOLLOW、os.O_DIRECTORY、os.O_NONBLOCK 和 os.getuid 都是 POSIX 属性。在不定义该属性的宿主上直接引用,会在下一行的"有界私有普通文件"校验之前抛出 AttributeError,调用方拿到的是无类型的崩溃,而不是它自己的拒绝,或者它本该产出的诊断。

三个读取处都受影响:

位置 为什么会走到
read_native_trace_file 没有任何平台门控,任何宿主都会调用;缺标志时原生观测探针根本不产出结果
read_state_recovery_audit 它前面的 _physical_ancestors 不做平台判定(verify_state_backup 才做),所以恢复回读先在 os.open 抛 AttributeError;即便绕过,紧随其后的 os.getuid() 也一样
checkpoint_retention._sync 已经判断 os.name == "posix",但没确认满足这一条件的宿主一定定义 O_DIRECTORY

证据

同仓库已有正确写法,loopx/ 下 21 个文件 30 处用 getattr(os, "<标志>", 0) 取这些属性(file_lock.py、completion_result.py、decision_capture.py、effect_runtime.py 等)。

在 POSIX 上删掉该属性即可复现同一分支:改动前新增的 7 个用例有 6 个失败,报错就是 AttributeError: module 'os' has no attribute 'O_NOFOLLOW',与原生 Windows 上的表现一致。

改动

  1. 三处改为读取宿主实际拥有的属性,再按各自的语义拒绝:
    • 恢复回读抛 recovery_input_unavailable,AttributeError 不再逃逸;
    • 观测读取在缺 O_NOFOLLOW 时显式拒绝符号链接,因为这时 os.open 无法替我们拒绝,诊断读取不应跟随任意目标。
  2. _sync 把平台门控和取标志放在同一处,两者不再各自表述。
  3. 恢复回读同时读取 os.getuid 的存在性,宿主没有所有者模型时给出自己的错误码,而不是等两行之后抛 AttributeError。

守卫

新增 tests/architecture/test_posix_flag_portability.py:用 AST 扫描 loopx/**/*.py,识别 os.<标志>、import os as x 别名、from os import <标志> 三种裸用;getattr 里的字符串不计入。

仍裸用的文件在 posix_flag_ceilings.json 里按文件记录上限(当前 5 个文件共 16 处),新增即失败;上限只降不升,降到更低不会被拒,这样正在改这些文件的 PR 不会因为修好了而被这条守卫挡回来。

测试

  • 新增 tests/control_plane/test_posix_flag_fallback.py:7 个用例,覆盖缺 O_NOFOLLOW、O_NONBLOCK、O_DIRECTORY、getuid 时的正常读取、拒绝符号链接、以及 _sync 跳过与其正向路径;改动前 6 个失败。
  • windows-powershell 新增一步,运行上面的守卫与这 7 个用例,让真实 Windows 分支在它发生的地方被验证。

范围:3 个读取处、1 个守卫、1 组用例、1 个 Windows lane 步骤。

@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=max.

动机 / Motivation

读取 Codex 原生诊断、历史恢复报告,或轮换 checkpoint 的操作者与 Agent。 在缺少 POSIX 属性的宿主上,原读取器先抛 AttributeError;改动使普通诊断可读、恢复缺所有者模型时返回类型化拒绝。 这能让诊断和恢复失败更可解释,但目标还包括保留原来的安全文件读取与 checkpoint 恢复保证。已独立验证普通文件与既有恢复流程,但无标志路径的链接安全和目录同步仍未满足。 不启用追踪、不运行模型、不改变 Goal、调度或配额,也不资格化新的恢复平台。

本次独立审阅完整 exact head 71d4ac4cb5cdc44c717ff163ffbf282575543d56,不可变 base bd79adc649680818f44f7c5f7e5743645309fc3d。没有把另一份评审、作者测试或 CI 元数据当作独立验收。

改动思路 / Approach

三个已有 IO owner 的放置与复用合适;缺失能力不能把安全拒绝或持久化步骤降为成功。 当前 PR 应交付这三处安全的跨平台降级与同一组守卫;修复 descriptor 身份校验和目录同步即可,无需新协议、状态或执行器。 Python 继续负责平台 IO,原 TypeScript artifact owner 继续选择、验证 checkpoint;不引入第二套调度、计分或生命周期权威。普通缺属性输入不再崩溃这一点有正向证据,但不能用“能打开”代替安全打开,也不能用函数返回代替同步完成。

独立同源场景 base 当前 head
原生 flags、正常输入/轮换 普通读取;删除2并进行6次 bank/trash fsync 相同
缺 no-follow、静态链接 AttributeError 类型化拒绝
缺 no-follow、检查后替换链接 拒绝/不读取 读到外部合成内容,真实 native observer 返回 true
POSIX 仅缺 directory flag AttributeError 中断 删除2成功,但6次请求中0次 bank/trash fsync

这些是实际文件 IO、原 TypeScript owner、rename、receipt 和 unlink 的观察;只注入能力存在性与文件替换的调度点,没有提供伪造的 owner 返回值。native observer 使用未改的 repository fixture,不运行真实模型。这张表不是原生 Windows 或断电试验的替代。

具体改动 / Changes

七文件共 +317/-5,其中三生产文件 +35/-5;新增 AST guard、fixture、七个 runtime 用例与一个 Windows workflow 步骤。全部三提交具备 DCO。审查前先读 accepted spec_ref: docs/reference/trajectory-assessment.md,spec_revision: bd79adc649680818f44f7c5f7e5743645309fc3d;同时读同一不可变版本的 docs/reference/configuration-backup.md 与 docs/reference/research-checkpoint.md。按其原文标题逐项映射:

  • Recorded Turn packet estimate:显式、本地、私有的原生诊断边界仍适用;F1 的外部 payload 读取使安全性 not_met。
  • Inspect a complete backup without activating it:缺 owner 模型的 typed refusal 与不激活 Goal 得以保留,但 _regular_input 的同类 fallback 竞争仍使安全读取 not_met。
  • Default bank rotation:选择/私有 bank 和原 receipt 流程保留;缺属性时取消目录同步使原恢复 transport 保证 not_met,见 F2。

关键代码讲解

  • read_native_trace_file 在读取前获取 flag、限制 regular file 与字节数,并由原生 context observer 消费;新增 is_symlink 只检查打开前的路径,未绑定实际 descriptor。
  • _regular_input 保留 owner、权限、长度检查,缺 getuid 提前返回 recovery_input_unavailable;缺 no-follow 时与前项存在同一窗口。
  • _sync 位于 quarantine rename 与原 receipt/后续 unlink 之间,and directory 让缺 flag 的 POSIX 调用直接成功返回。
  • bare_flag_uses 扫描裸属性、os alias 和 from-import,fixture 允许当前5文件16处引用下降。它与新增 Windows step 都有实际 pytest consumer;这是本地测试 vocabulary,不扩展产品协议或新增权限。

既有 loopx/file_lock.py 已使用 O_RDONLY | getattr(os, "O_DIRECTORY", 0) 后真正同步目录,是 F2 可复用的较小修复模式。_no_follow_flag 只有一个生产调用方,可考虑内联;这只是非阻塞简化建议,不能替代两项物理语义修复。

对主干的风险 / Risks

[P2] F1:关闭检查到打开之间的链接替换窗口

在缺 O_NOFOLLOW 的分支,path.is_symlink() 与 os.open() 分离。把合成 payload 在这两步之间换成 bundle 外的 symlink,当前函数读到了外部 bytes;通过真实 codex_context_in_active_input,在原有 resolve/containment 检查之后做同一替换,当前 head 返回 true。保留 native flag 的相同控制返回 false,静态 symlink 也返回 false,所以新增静态链接测试捕获不到它。恢复 _regular_input 共享此模式。请在任何读取前绑定平台安全 handle 与实际文件身份,不能建立保证时按 owner 类型化拒绝;添加这个确定性调度反例并保留正常文件用例。

[P2] F2:缺目录 flag 时不要把同步变成成功空操作

_state 仍可把该 POSIX transport 判为合格,随后 _sync 因缺 O_DIRECTORY 静默跳过。用三个真实 checkpoint、保留最新一个的策略走完整 rotate_bank(execute=True),当前 head 删除两个并返回成功;bank/trash 六个同步请求没有一次调用 fsync。正常能力的同一流程有六次,base 缺属性则中断而非报告完整成功。这里确认的是同步步骤丢失,未声称已经跑过断电或观察到数据丢失。请保留安全目录 descriptor 的 fsync,或在第一次移动前拒绝不合格 transport;不能在移走恢复点后把同步省略视为成功。

独立本地检查 结果与实际范围
新 portability/AST tests PASS:head 11项;同一七个 runtime case 在 base 为6个 AttributeError FAIL、1 PASS
原三个完整相关模块 PASS:base/head 各167项,无skip;含真实 File/SQLite/HTTP 恢复、native input 和 checkpoint retry
新真实入口反例 FAIL:F1 外部 payload 被接受;F2 删除成功但目录同步为0;普通能力控制保持
changed-file Ruff PASS:五个 Python 文件
configured Ruff / mypy FAIL:两端完整697B E731、208B no-any-return 输出分别逐字相同,失败源码/config blob未改;required失败保留
standard native premerge FAIL:6 direct通过,12 selected中11通过;唯一 CLI-output differential 失败,本轮未作独立 base 归因,保持 unresolved
exact-scope change quality FAIL:cqr_187826005a531df5ef88,verify invalid_receipt,没有 safe fix 或 required-check 豁免

语义与 CI 对齐

本次复用原 IO 与 artifact owner,不建立新的共享 state vocabulary。diff-scoped semantic advisory 未发现支持的新增生产 carrier,完整 semantic canary 通过;这不证明物理 IO 等价,F1/F2 仍是当前合同的阻塞。未查询、轮询或等待远端 CI。新增 Windows 命令只在当前 macOS 源环境执行,未宣称原生 Windows 合格;其 owner-model 与 directory 条件 skip 也不能证明对应 native 路径。

我的整体评价 / Verdict

REQUEST_CHANGES。原崩溃真实且维修边界合理,但不能批准目前的 fallback:无 no-follow 时成功读取竞态外链,无 directory flag 时成功取消持久化步骤。长程恢复保证与完整操作体验目前均为 not_yet_proven;保持正常能力的既有167项通过不等于这两个新分支安全。只改文件 IO、测试和 workflow,没有渲染页面;已检查恢复 API、原生诊断入口和 retention CLI 的既有消费者。 未运行原生 Windows、实际模型或断电恢复;能力删除模拟、真实文件 IO 与同步调用观测不能替代这些资格。

三个已有 IO owner 的放置与复用合适;缺失能力不能把安全拒绝或持久化步骤降为成功。 当前 PR 应交付这三处安全的跨平台降级与同一组守卫;修复 descriptor 身份校验和目录同步即可,无需新协议、状态或执行器。 无需新页面、协议或整套平台重写;在原分支完成两处 bounded 修复并加入实际入口负例,按新 exact head 重审。必需静态、premerge 和 CQR 红灯单独保留,不要求此 PR 顺便修改无关旧代码。没有合并或修改作者源码。

English verdict: REQUEST_CHANGES — 71d4ac4: the missing-flag fallbacks admit a raced outside symlink and silently skip checkpoint directory fsync; ordinary tests pass, but the two real-path counterexamples and required quality holds remain.

# Without O_NOFOLLOW the open cannot refuse a symlink for us. Refuse it
# here rather than letting a diagnostic read follow an arbitrary target.
raise ValueError("native observation must not be a symlink")
descriptor = os.open(path, os.O_RDONLY | no_follow | getattr(os, "O_NONBLOCK", 0))

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.

[P2] 缺 O_NOFOLLOW 时请绑定实际打开的安全 handle

这里的 is_symlink 检查与 os.open 之间可被替换。独立测试在真实 open 前把已检查的 payload 换成 bundle 外 symlink:函数读到外部合成内容,真实 codex_context_in_active_input 返回 true;保留原生 flag 的相同控制则拒绝。静态 symlink 用例无法覆盖这个窗口,_regular_input 也有同类 fallback。请在任何读取前使用平台 no-follow/reparse-safe handle 并验证实际文件身份,不能保证时按原 owner 类型化拒绝;加入确定性替换反例。

Comment thread loopx/hosts/checkpoint_retention.py Outdated
# O_DIRECTORY is POSIX-only, so the platform gate and the flag lookup must
# name the same thing rather than trusting `os.name` alone.
directory = getattr(os, "O_DIRECTORY", 0)
if os.name == "posix" and directory:

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.

[P2] 缺 O_DIRECTORY 时不能静默省略目录同步

_state 仍会接受该 POSIX transport,而这个条件让每个同步请求直接返回。独立运行真实三 checkpoint/latest-one 轮换,去掉 O_DIRECTORY 后 head 删除两个并报告成功,bank/trash 六个请求零次 fsync;正常能力控制实际执行六次。这里不等于做过断电测试,但原持久化步骤确已丢失。可复用 file_lock 既有安全目录 fd/fsync 模式,或在首次 rename/delete 前明确拒绝不合格 transport,并用真实轮换断言同步或无副作用拒绝。

@gcl-coder

Copy link
Copy Markdown
Contributor Author

两处 bounded 修复已推上新的 head(063ea7dff),未引入新协议、状态或执行器。

F1 缺 O_NOFOLLOW 时不再有检查与打开之间的窗口

原写法是先 path.is_symlink() 再 os.open(),两次调用之间目录项可被替换。现在改为先打开、再绑定:拿到 descriptor 后比较 os.fstat(fd) 与 os.lstat(path) 的设备号与 inode。链接打开的是目标,其身份与链接自身不同,因此在读取任何字节之前就被拒绝;这在内核拒绝链接之外,给出了同一个保证的位置——拒绝发生在上游读到第一个字节之前。

两处共享该规则:read_native_trace_file(新增 _open_verified,按建议把只有一个调用方的 _no_follow_flag 内联删除了)与 state_recovery_io._regular_input。

反例是确定性的,在打开那一刻替换目录项:

用例 旧 head 71d4ac4cb 新 head
test_native_trace_read_refuses_a_link_swapped_in_at_the_open FAIL expected ValueError, nothing raised PASS
test_recovery_readback_refuses_a_link_swapped_in_at_the_open FAIL expected StateRecoveryError, nothing raised PASS
test_trace_link_swapped_after_containment_is_refused_before_the_read(经真实 codex_context_in_active_input) FAIL(返回 true,即接受外部内容) PASS
test_native_trace_read_refuses_the_same_swap_with_the_flag(原生 flag 控制) PASS PASS

最后一条走的是真实入口:fixture 不变,只在 trace.jsonl 被打开的一瞬把它换成 bundle 外的链接,旧 head 上 codex_context_in_active_input 返回 true,与你独立复现一致。

F2 缺 O_DIRECTORY 时仍然同步

_sync 不再把缺属性当成功。现在沿用 file_lock.write_json_atomic 的既有模式:os.open(path, O_RDONLY | getattr(os, "O_DIRECTORY", 0)),用 fstat 确认这个 descriptor 确实是目录(补上 O_DIRECTORY 原本提供的类型保证),再 fsync(fd);不是目录则抛 ValueError。os.name != "posix" 的早退保留,那里的 transport 本来就未合格。

真实轮换用例 test_rotation_flushes_directories_without_the_directory_flag:删掉 O_DIRECTORY,真实 checkpoint、execute=True、keep_latest=1,断言 deleted 非空且目录 fsync 至少两次(bank 与 trash 父目录)。旧 head 上该用例失败于 the directory must be flushed, not silently skipped,与该处报告同步步骤丢失一致。

本地验证范围

  • pytest tests/architecture/test_posix_flag_portability.py tests/control_plane/test_posix_flag_fallback.py tests/control_plane/test_codex_input_retention.py → 107 passed
  • ruff check --select E4,E7,E9,F 对本 PR 改动的五个 Python 文件 → All checks passed
  • 未在本机跑原生 Windows;Windows 步骤仍以 CI 为准

保持原状的项

  • kernel-static-checks 的 E731 位于 tests/test_manager_context_roundtrip.py:289,由主干 acecb5d6b (#8056) 引入,不在本 PR 改动内;已另开 fix(tests): spell the steward evidence sender as a def #8084 单独修,本 PR 变基后即可恢复。
  • CLI-output differential premerge 与 cqr 回执两条红未见与本改动相关,未在本 PR 内顺带修改。

@gcl-coder
gcl-coder force-pushed the fix/portable-native-trace-flags branch from 71d4ac4 to 11691d7 Compare November 5, 2026 03:21

@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=max.

动机 / Motivation

读取原生诊断、检查历史恢复报告与轮换 checkpoint 的操作者和 Agent。 宿主缺少 POSIX 属性时,原代码先抛 AttributeError;当前改为安全读取普通诊断或原 owner 的类型化拒绝,并保留真实目录同步。 实际文件读取与轮换已验证修复上轮两个 P2;新增 Windows 检查仍会因两项错误的平台断言失败。 不启用追踪、不执行模型、不激活备份或改变 Goal、权限、调度、配额与 checkpoint 选择策略。

本次重审完整 exact head 11691d7a8f070f5ee7add6495a3a27c6674ed4c9,不可变 merge base c6ab5afc174dfbfa2354e2c66d7431b47ac79151。目标分支已继续前进,使用真实 merge base 审阅九文件,而非把当前 main 到 head 的无关历史逆向差异算进本 PR。上轮 71d4ac4cb5cdc44c717ff163ffbf282575543d56 的两个 P2 分别映射到当前修复并重新验证,没有继承旧结论。

改动思路 / Approach

Python 保留平台 IO,既有 TypeScript owner 保留 checkpoint 选择与验证;没有平行状态或执行权威。 本 PR 的边界仍是三处安全跨平台 IO 与守卫;当前只需为两个 POSIX 同步断言补平台条件,并保留非 POSIX 的 no-op 验证。 普通缺属性输入能安全继续,比仅把异常包成错误有实际价值;直接使用 getattr 而省略安全或同步已被上轮反例否定。当前 fd 身份检查与既有 file_lock 目录同步模式解决了这两点,无需通用平台重写。下表是独立执行的行为比较,不是原生 Windows 或断电证明。

同源实际场景 不可变 base 当前 head
普通能力读取、轮换 正常;删除2、目录 fsync6 保持
缺 no-follow,普通诊断 AttributeError / observer false 安全读取 / observer true
缺 no-follow,打开时换外链 不可用 两读取器拒绝,真实 observer false
POSIX 仅缺 directory flag AttributeError,中断原轮换 删除2、保留最新、实际目录 fsync6
新同步测试的 nt 分支 本 step/test 组合不存在 两项失败;posix 同样调用均通过

具体改动 / Changes

完整九文件 +470/-9,四提交均有 DCO;最后提交增加 descriptor 与目录 entry 的身份比较、保留 fsync 及实际竞态回归。spec_ref: docs/reference/trajectory-assessment.md,spec_revision: c6ab5afc174dfbfa2354e2c66d7431b47ac79151;同时读同一版本 docs/reference/configuration-backup.md、docs/reference/research-checkpoint.md,三份文件与当前目标分支的 blob 一致。按原文标题映射:Recorded Turn packet estimate 的本地私有、不改执行权威边界 implemented;Inspect a complete backup without activating it 的私有普通文件、容量及 inert readback implemented;Default bank rotation 的既有选择、quarantine/receipt 恢复与实际同步 implemented。Windows 检查问题是新增测试与已有平台门不一致,另列 F1。

关键代码讲解

  • _open_verified 在真实 open 后、读 bytes 前核对 fd 与 lstat 的 device/inode,并拒绝 symlink;同一未改 fixture 函数驱动真实 observer,正常缺标志可用、竞态外链返回 false。
  • _regular_input 保留 owner、私有权限、regular 与长度检查,缺 getuid 先类型化拒绝,同样在读取前绑定身份。其 IO 异常仍由现有 CLI/API 处理。
  • _sync 仅在 POSIX 验证目录 fd 后真实 fsync。三点/latest1 走完整 TS owner、rename、receipt、unlink,缺目录 flag 也实际同步六次;未改 non-POSIX 的提前返回。
  • 同步 fallback 测试 与相邻“非目录必须 ValueError”用例没有 POSIX 条件,却由新增 Windows step 一起执行。这是当前剩余阻塞;AST ceiling 仍是测试本地 vocabulary,没有新产品协议。

对主干的风险 / Risks

[P2] F1:把两个目录同步断言限定到其 POSIX 合同

Windows 上 os.name == "nt",生产 _sync 按设计立即返回;但第一个新测试要求 flushed == [True],相邻测试要求 ValueError。新 Windows workflow 无条件执行整个文件,也没有全局 collection skip。独立调用未改的两个 repository 测试函数,只选择 posix/nt 分支、保留实际文件系统和真实 _sync:posix 两项通过,nt 分别报 the directory must be flushed, not silently skipped 与 DID NOT RAISE ValueError。这说明 macOS 的15项通过不能证明新 Windows 命令可用,并未冒称执行了原生 Windows。

最小修复:为这两个 POSIX 同步断言增加平台 skip,另保留 non-POSIX no-op 的明确测试,再在 Windows 执行新增的原命令。不要为满足测试而放开生产 rotation 的平台资格。上轮 F1 外链竞态和 F2 无声省略 fsync 已有新的真实反例重放证明修复,未作为本轮重复 finding。

独立验证 当前结果
新 portability / AST PASS:15项,当前 macOS
三个完整相关模块 PASS:base167 / head169,无 skip;含实际 File/SQLite/HTTP 与 quarantine retry
真实 IO / observer / 轮换 PASS:外部静态/竞态链接拒绝;缺 directory 时6次 fsync,普通能力保持
精确 nt 测试分支 FAIL:两个原函数失败;只替换 OS 分支,不伪造返回值
changed Python Ruff PASS:全部七个改动 Python 文件
configured Ruff / mypy FAIL:两端完整697B E731、208B no-any-return 分别逐字相同,失败 source/config 未改;required 红灯保留
native premerge / CQR FAIL:6 direct通过,selected11/12通过;CLI-output 未作本轮独立 base 归因;cqr_d14475ccdf57552b904c 为 fail

语义与 CI 对齐

没有新共享 state vocabulary,diff-scoped advisory 的三生产文件未检出支持的 carrier,完整 semantic canary 通过;这些不证明平台测试正确。当前 F1 直接违反新增 Windows 检查与已有 _sync 平台门的对应关系。未查询、轮询或等待远端 CI,也未把以前或本轮未知失败改为通过。

我的整体评价 / Verdict

REQUEST_CHANGES:三处运行时维修已有正向证据,上轮两个 P2 已修复;完整当前 PR 仍需修正新增 Windows 测试的平台条件。长程恢复边界在已测本地路径中 preserved,普通诊断和拒绝体验 improved;没有据此声称实际 Host 采用或净效率改善。只修改平台 IO、pytest 与 workflow,已检查原生诊断、恢复 API 和 retention CLI;没有渲染页面变化。 未执行原生 Windows、真实模型或断电恢复;OS 分支模拟与 fsync 观测不等于这些资格。

Python 保留平台 IO,既有 TypeScript owner 保留 checkpoint 选择与验证;没有平行状态或执行权威。 本 PR 的边界仍是三处安全跨平台 IO 与守卫;当前只需为两个 POSIX 同步断言补平台条件,并保留非 POSIX 的 no-op 验证。 这是同域、可逆的修复,不需要另起实现、协议、页面或任务。strict CQR、已有静态失败、CLI-output 失败与后续 merge-readiness 保留各自门;未修改作者源码或合并。

English verdict: REQUEST_CHANGES — 11691d7: the earlier link-race and missing-fsync defects are fixed, but the new Windows step runs two unguarded POSIX-only sync assertions that fail on the Windows branch.

pass


def test_checkpoint_sync_flushes_the_directory_without_the_flag(tmp_path, monkeypatch):

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.

[P2] 请把这两个目录同步断言限定到 POSIX

新增 Windows step 会无条件运行本文件,但 Windows 的 os.name 为 nt,_sync 在现有平台门直接返回。这里因此拿到空 flushed;相邻“非目录必须 ValueError”测试也不会抛错。独立调用未改的两个测试函数、只选择 posix/nt 分支:posix 均通过,nt 分别触发此断言与 DID NOT RAISE ValueError。请将两个 POSIX 断言标记为 POSIX-only,另保留 non-POSIX no-op 用例,并运行原生 Windows 命令;不要放开生产平台资格来迁就测试。

O_NOFOLLOW, O_DIRECTORY and os.getuid are POSIX-only attributes. Spelling one
directly raises AttributeError on a host that does not define it, and it raises
before the bounded private regular-file check on the next line can refuse
anything, so each of these readers reported an untyped crash in place of its own
refusal:

- read_native_trace_file is reachable on any host and had no gate, so the
  observation it exists to produce never appeared.
- the recovery readback is reachable without the platform gate verify_state_backup
  applies, and its os.getuid() failure came after the open had already succeeded.
- checkpoint_retention._sync already tested os.name == "posix", but never asked
  whether the host that satisfies that test also defines O_DIRECTORY.

Read the flag this host has, as the peer, completion, decision-capture and
effect-runtime readers already do, and refuse a symlink explicitly where the
open cannot. The recovery reader reports its own recovery_input_unavailable
instead of letting AttributeError escape.

Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com>
A bare os.O_NOFOLLOW, os.O_DIRECTORY or os.O_NONBLOCK raises AttributeError on
a host that does not define it, and it raises before the bounded regular-file
check on the next line can refuse anything. The repository already reads these
flags with getattr in the peer, completion, decision-capture and effect-runtime
readers.

Pin every remaining site per file so a new one needs a recorded reason, and
assert the readers this change fixed stay at zero. A lower measurement is
accepted, so retiring one of these sites does not have to edit a fixture that
would then describe a tree it no longer matches.

Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com>
Deleting os.O_NOFOLLOW, os.O_NONBLOCK, os.O_DIRECTORY or os.getuid for the
duration of one call is the branch a host that lacks them takes. Six of these
seven cases failed on the pre-fix tree with
`AttributeError: module 'os' has no attribute 'O_NOFOLLOW'`, which is the same
failure a native Windows host reports, so the cases reproduce the defect on a
POSIX checkout.

The Windows lane runs the guard and these cases as well, so the branch a real
Windows host takes stays qualified in the place it is reached.

Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com>
Two holds were open in the previous fallback.

Reading without O_NOFOLLOW checked the path and then opened it, so the entry
could be replaced by a link to another file in between: nothing was bound to
what was actually opened. Open first, then compare the opened descriptor with
the directory entry before one byte is read. A link opens to its target, whose
device and inode differ from the link's own, so a swapped or linked entry is
refused instead of followed. The native trace reader and the recovery readback
share the rule.

checkpoint retention treated a missing O_DIRECTORY as permission to skip the
flush, so a POSIX host without the flag deleted recovery points and reported a
rotation that never became durable. O_DIRECTORY only hardens the open: take the
flag the host has, refuse a descriptor that is not the directory, and flush it,
as file_lock.write_json_atomic already does.

Static-link cases could not see either defect. The new cases replace the entry
at the instant it is opened, and one runs through
codex_context_in_active_input, so head before this commit fails them.

Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com>
The new native Windows step ran the whole fallback file, but two sync
assertions describe a POSIX bank: _sync returns before flushing when the
host is not POSIX, so the flush list stayed empty and the non-directory
target raised nothing. Mark those two POSIX-only and pin the other branch
with an explicit no-op case that holds on both hosts.

Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com>
@gcl-coder

Copy link
Copy Markdown
Contributor Author

F1 已修,只动测试,没碰生产平台的资格判断。

  • test_checkpoint_sync_flushes_the_directory_without_the_flag 和 test_checkpoint_sync_refuses_a_target_that_is_not_a_directory 加了 POSIX_SYNC(os.name != "posix" 时 skip),reason 写明 _sync 只对 POSIX bank 成立。
  • 新增 test_checkpoint_sync_claims_nothing_off_posix:monkeypatch.setattr(os, "name", "nt") 后对目录和普通文件各调一次真实 _sync,断言既不抛错也不调用 os.fsync。这条在 POSIX 与 Windows 上都成立,新增的 Windows step 因此仍覆盖 nt 分支,不是只剩 skip。
  • _sync 的 if os.name != "posix": return 与其它生产代码未改。

本地(macOS/posix):test_posix_flag_fallback.py + test_posix_flag_portability.py 共 16 项通过;nt 分支单独模拟,fsync 调用 0 次、无异常;ruff --select E4,E7,E9,F 对这两个文件全绿。

以上是 macOS 上的结果,不代替原生 Windows 证据;那条要等这个 head 的 windows-powershell lane 跑完。

@gcl-coder
gcl-coder force-pushed the fix/portable-native-trace-flags branch from 11691d7 to 99e9b5d Compare November 5, 2026 07:26

@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.

No blocking findings. 本次重新检查完整 exact head,未继承旧结论。

Reviewer: model_agent; model=gpt-6.1-sol; provider=OpenAI; reasoning_effort=max; declaration_source=runtime_reported; execution_observation_id=32dd9a2462f888f9986adfde40a0273a4fd158866b2a5abcd0acb166065f0a17.

动机 / Motivation

在缺少部分 POSIX 文件标志的主机上读取本地诊断、恢复报告或维护检查点的用户会遇到这个问题。 以前,读取一个合法本地文件可能在安全检查前直接抛出 AttributeError;现在普通文件可读,换入的符号链接被拒绝,合格 POSIX 目录仍完成刷新。 本次对照验证了缺失标志分支的可用性和拒绝行为,并确认此前两项 IO 问题及 Windows 测试条件问题已在当前 head 修复。 本 PR 不扩大备份激活、检查点删除权限或诊断启用范围,也不证明实际 Windows 运行资格、断电耐久性或模型收益。

改动思路 / Approach

文件安全和耐久性是既有 IO 不变量,必须在原 reader/transport 内成立;不是新的可选策略,也不产生第二个决策 owner。 仅三个既有 Python IO 入口、回归用例、局部静态防退化上限和 Windows 测试步骤;保留 TS 策略与平台门。 入口仍是原生诊断读取、隔离恢复和显式轮换;打开文件后的实际 descriptor 元数据与路径 entry 进行设备号和 inode 核对,再执行原有 regular/private/size 检查。这样既保留正常读取,也拒绝换入的链接。只有既有平台资格允许的轮换才能到达目录刷新;能力存在本身不会启动诊断或删除任务。

两个 reader 的错误类型、owner 条件和资源生命周期不同,局部复用 os/stat 模式比抽出可配置安全文件框架更窄。新增 _open_verified 与 _require_directory 是相邻职责内的有限整理,未复制 TypeScript 策略。下表仅解释源码行为,不作为执行证明;本次没有新增图形或 Mermaid 渲染要求。

触发条件 旧路径 当前 head
主机缺失文件 flag 属性访问先失败 探测 flag 后继续既有资格检查
缺失 no-follow 且 open 时换入链接 旧 fallback 曾存在读取窗口 descriptor/entry 绑定,读取前拒绝
POSIX 缺失 directory flag 旧实现失败或旧 fallback 跳过刷新 证明目录类型后仍 fsync
非 POSIX 不声称目录刷新 保持无操作

具体改动 / Changes

评审范围是 e7904f97cdec03888c4b0f376f03c33d51d5182c...99e9b5d5ed5f0a9ce7b9ce206aa6a16888198041:9 个路径,487 行增加、9 行删除。生产代码仅三个文件;其余为 Windows workflow、一个本地 AST 债务上限和持久回归。该测试会扫描 os alias 和 from-import,不依据业务描述文字分配权限;记录的 16 个既有 bare sites 没有在本 PR 中扩大,降低计数不需要把旧上限重新抬高。

关键代码讲解

  • _open_verified:请求主机具备的 flags;读取前绑定 descriptor 与当前非链接 entry。输入或前态为 缺失 O_NOFOLLOW/O_NONBLOCK、换入链接;结果由 read_native_trace_file 消费,失败由 ValueError/OSError 并关闭 descriptor 处理。

  • _regular_input:无 owner model 时拒绝;保留私有 regular-file 和容量检查。输入或前态为 缺失 getuid/no-follow、链接、非私有或超界;结果由 恢复 audit/verify 消费,失败由 StateRecoveryError;失败关闭 fd 处理。

  • _sync:非 POSIX 明确无操作;POSIX 缺少 O_DIRECTORY 时仍检查目录并 fsync。输入或前态为 合格 bank 已进入轮换;结果由 真实 rotation receipt 消费,失败由 异常阻止成功回执,finally 关闭 fd 处理。

规格与验收映射

先读取改动前接受的文档:docs/reference/trajectory-assessment.md; docs/reference/configuration-backup.md; docs/reference/research-checkpoint.md;spec_revision=e7904f97cdec03888c4b0f376f03c33d51d5182c。这里依据既有局部 IO 契约判断,不要求这项兼容修复完成完整恢复或研究平台路线图。

  • trace-private-bound — implemented:Native tracing remains local, opt-in and private, with no result, authority or model-execution change. 对应 loopx/control_plane/turn_driver/codex_input_trace.py;验证:185 tests plus independent missing-flag/swap probe。
  • recovery-inert-input — implemented:Source archive and manifest must be private regular files; only a new inert checkpoint may be published. 对应 loopx/control_plane/state_recovery_io.py;验证:real File/SQLite recovery suite and recovery no-follow probe。
  • retention-posix-durable — implemented:Execution requires a private operator-owned bank on the qualified POSIX transport; recovery and retained points remain protected. 对应 loopx/hosts/checkpoint_retention.py;验证:real rotation, pin/recovery negatives, missing-directory-flag fsync and nt no-op。

此前评审的两个 runtime 问题和 11691d7a8f070f5ee7add6495a3a27c6674ed4c9 的 Windows 测试问题分别复核:当前 head 给两项 POSIX-only 刷新断言加平台条件,并另测 nt 无操作;生产 _sync 平台门没有放宽。不是简单删除失败断言来获得通过。

对主干的风险 / Risks

最强风险仍是无 flag 时跟随链接、丢失目录刷新或绕过私有/容量条件。本次独立的真实文件探针在固定 base 复现 AttributeError,在 head 验证原字节读取、换入链接拒绝、真实目录 fsync 与 nt 无调用;185 项相关测试覆盖真实 File/SQLite 隔离恢复、完整轮换、pins、未知文件及 quarantine 恢复。固定 base 的原有集合为 167 passed;新增回归与完整现有行为一并验证,不能只用新增测试数量证明安全。

TypeScript typecheck、仓库声明范围 Ruff、mypy(17 sources)及 standard premerge 通过,预合并报告保留一个继承的 maintainability advisory;没有调高上限或豁免必需检查。最初误写测试路径的一次调用未执行任何测试,已改为真实 tests/test_state_recovery.py 路径后完成上述集合。没有查询或等待远端 CI。原生 Windows、真实断电和最新 main 集成后的运行没有在本地重新资格化;本次结果不能替代它们。

语义与 CI 对齐

复用原平台与传输语义;新增 ceiling schema 是 local_only 测试词汇。diff-scoped semantic advisory 未识别新的生产闭集,但这不等于自动证明所有语义。新的测试修复提示没有移除原私有、容量、权限、停止或启用条件;actual no-flag/nt 反事实与源码分别核对。诊断关闭、恢复未调用和保留未启用时没有新增文件状态、计时器、模型调用或 quota 行为。实际 Windows CI 仍由其原 runner 判定。

我的整体评价 / Verdict

交付判断 goal_achieved 仅限本次可移植 IO 修复。long_horizon=preserved;user_experience=improved:合法文件不再因属性缺失先崩溃,非法输入仍沿有界资格拒绝。已检查三个内部文件传输路径和 Windows CI 步骤;没有渲染页面、布局或可视交互变更。 代码体量与该安全边界相称,现有 owner 和局部 helper 足够,无需另建框架。严格 change-quality 记录与验证的最终结果单独保存;此前失败记录没有被改写成通过。

原生 Windows、真实断电、模型采用和长期成本收益仍未知;这些限制与本地已验证结论分开。该 exact-head 评审不授予 merge、review dismissal、安装或发布权限。

English verdict: APPROVE — no blocking finding remains at 99e9b5d; native Windows and power-loss qualification remain unverified.

@huangruiteng
huangruiteng merged commit aa82591 into loopx-project:main Nov 5, 2026
33 of 43 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