fix(extensions): bind the declared validator implementation - #4850
Conversation
c4872a7 to
176bd0c
Compare
huangruiteng
left a comment
There was a problem hiding this comment.
Request changes conclusion (author-owned PR; GitHub blocks formal self-review)
动机
这个 PR 是 #4817 的 post-merge repair。#4817 把 extension-owned presentation validator 从 LoopX parent process 移进扩展自己的隔离 Python runtime,但 executable provider 的 doctor identity 只覆盖 launcher,没有覆盖后来真正执行 validator 的 Python。因此只替换 shebang/sibling Python、保持 provider wrapper 不变时,catalog 与 binding 仍会错误地认为 runtime 已经过 doctor。
本 PR 对那个精确反例做了正确修复:把 provider launcher digest 与 selected Python interpreter digest 合成一个 runtime identity,并让 install/doctor、catalog/binding stale check 继续复用同一持久化字段。没有新增第二份 readiness 状态,也没有扩大 extension 权限。
改动思路
_runtime_executable_identity 以 canonical JSON 组合 entrypoint_identity 与 python_interpreter_identity;resolve_runtime_entrypoint 对 shebang Python 和 opaque sibling Python 都返回该 composite。非 Python executable 仍沿用 launcher-only identity,python_module runtime 仍沿用既有 interpreter + module identity。
这条 owner 选择是对的:extension_doctor 在 probe 前后计算同一 identity,_verified_entrypoint 在 catalog/binding/presentation 使用前重算并比较,变化时走现有 doctor readiness is stale; run loopx extension doctor ... --execute 恢复路径。
但“实际执行 validator 的完整效果边界”仍没有被这份 composite 覆盖。隔离 Python 会根据 manifest 的 view_validator 动态 import extension site-packages 中的模块;该模块可以独立变化,而 launcher 和 Python binary 都不变。
具体改动
精确 head 176bd0cd4f87d1e3d6fff580d1717eff80903e57 改 2 个文件、+123/-5;与上一 head 的 range-diff 显示 patch 完全一致,我仍在新 base 上重跑了完整聚焦验证与反例:
loopx/extensions/readiness.py:新增 35 行 composite identity 逻辑;tests/extensions/test_extension_runtime.py:新增 88 行,覆盖 opaque launcher、shebang interpreter mutation,以及真实 install → doctor → catalog/binding stale 生命周期。
关键代码讲解
-
_runtime_executable_identity只把 launcher 与 interpreter 两个 digest 放进 composite。它修复了此前的 interpreter-swap 缺口,但这里也是剩余 blocker 的位置:manifest 已声明的view_validatorimplementation 没进入 identity。 -
resolve_runtime_entrypoint返回同一个python_executable给 presentation 层执行,并把 interpreter bytes 纳入 readiness identity;路径/venv module tree 本身没有进入摘要。 -
_verified_entrypoint只要 active revision 与上述 composite 相等就返回 ready;因此它无法观察 site-packages validator module 的变化。 -
load_presentation_view_validator随后用这个 Python 执行-I -c,并动态 importview_validator。这证明 validator implementation 不是旁路数据,而是实际决定 projection 接受/拒绝的 effect artifact。
现有正向验证我完整重跑了:三个 focused suites 共 132 passed in 52.39s;Ruff、mypy 与 git diff --check 均通过。它们足以证明 interpreter mutation 修复生效,但没有覆盖下面的独立 module mutation。
对主干的风险
[P1] doctor identity 仍未绑定实际执行的 validator implementation
我在 exact head 上用真实临时 venv 做了 install/doctor 生命周期反例:provider wrapper、venv Python 路径与 Python binary 都保持不变,只把 site-packages 中 manifest 声明的 validator module 从返回 marker=a 改成返回 marker=changed-with-more-bytes。结果是:
install_verified = true;- mutation 前后
ResolvedRuntimeEntrypoint.identity完全相同; extension_status(...).doctor_verified前后都为true;- 隔离 validator 的实际输出已经变化。
也就是说,当前 patch 修复了“解释器被替换”,却仍允许“在同一解释器里执行的决策代码被替换”而不使 doctor receipt 失效。catalog/binding 会继续投影 ready,未经当前 doctor 验证的代码却拥有 projection acceptance authority。这正是 #4817 trust boundary 的同一 change reason,不是无关的增强要求。
最小修复不是另造 lifecycle 或 receipt,而是让现有 composite 同时绑定 manifest-declared view_validator 的实现工件(或能够完整代表该 versioned installed runtime 的 distribution/environment identity),并补一条真实回归:install → doctor → ready → 仅修改 validator module → catalog/binding/publish 在执行新 validator 前 fail stale;未修改 module 的对照仍应 ready。
远端 CI 在 capability packet 生成时仍有 pending 项,但即使之后全绿也不能覆盖这个已复现的真实边界反例。
我的整体评价
当前实现对上一轮明确指出的 interpreter-swap blocker 是有效且比例合适的修复,复用了正确的 owner,测试质量也不错;我不建议回退到 launcher-only identity,也不建议拆出新的 validator readiness 系统。
但 #4850 的目标是修复 #4817 的 doctor-verified validator runtime 边界。只绑定 Python binary、遗漏实际被 import 并执行的 validator implementation,仍会产生 false-ready。因此本 exact head 结论为 REQUEST_CHANGES。补齐同一 composite 与真实生命周期 mutation 后可以重新审查。
English verdict: REQUEST_CHANGES - exact head 176bd0c; the rebased patch is range-diff identical to the prior head, launcher-plus-interpreter binding fixes the prior interpreter-swap reproducer, and 132 focused tests plus Ruff, mypy, and diff check pass, but a real install/doctor venv mutation keeps the identity and doctor_verified state unchanged while the declared validator module changes its output, so unverified decision-bearing code can still execute as ready.
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
176bd0c to
728d74a
Compare
huangruiteng
left a comment
There was a problem hiding this comment.
Request changes conclusion (author-owned PR; GitHub blocks formal self-review)
Exact head: 728d74abd9ad79ed911c8cf75155369cd451804d
动机
#4817 把 extension-owned presentation validator 移进扩展自己的隔离 Python runtime,这个方向正确;但它已在 exact-head review 仍为 REQUEST_CHANGES 时合入,遗漏点是 doctor identity 只覆盖 provider launcher,没有覆盖稍后真正执行 validator 的 interpreter。保持 launcher 和 interpreter 路径不变、只把 Python symlink target 换成 /bin/sh,旧实现仍投影 ready=true。本 PR 是对这个已验证 post-merge gap 的完整窄修复。
改动思路
继续复用 ResolvedRuntimeEntrypoint、_file_identity 和既有 doctor_verified_entrypoint_identity,不新增第二份 receipt:executable runtime 若选择了 Python,就把 launcher artifact identity 与 interpreter artifact identity 编码成 typed composite payload 再哈希;未选择 Python 的 executable runtime 保持 launcher-only identity。这样 doctor、catalog、binding、provider execution 与 projection publication 仍共享同一 authority owner。
具体改动
RUNTIME_EXECUTABLE_IDENTITY_SCHEMA_VERSION/_runtime_executable_identity:定义 launcher + interpreter 的内部复合身份;解释器存在但无法识别时 fail closed。resolve_runtime_entrypointexecutable 分支:先解析真实python_executable,再构造复合 identity,并把同一个 executable 交给 presentation validator。test_opaque_runtime_identity_binds_sibling_python_artifact与test_runtime_entrypoint_identity_binds_selected_python_artifact:固定路径和 launcher,只替换 interpreter artifact,断言 identity 改变。test_catalog_invalidates_doctor_when_selected_python_artifact_changes:走真实 install → doctor → catalog/binding 生命周期,证明 mutation 后ready=false且 binding 报 doctor stale。
关键代码讲解
_runtime_executable_identity 是唯一新增决策点:无 Python 时返回原 launcher digest;有 Python 时调用既有 _file_identity 解析最终 artifact 并把两个 digest 放进 canonical JSON。resolve_runtime_entrypoint 是共享入口,因此 doctor 持久化与后续 _verified_entrypoint 重算不会出现两套规则。生命周期回归不 mock readiness,它实际写临时 extension state 并从 catalog/binding 读取结果。
对主干的风险
有意行为变化是:已 doctor 的 Python executable runtime 在 launcher 或 interpreter 任一 artifact 变化后都必须重新 doctor;Python-module runtime、未选择 Python 的 executable runtime、manifest/schema/权限/CLI 都不变。相同路径的 symlink replacement 被内容身份捕获,路径字符串比较不足的问题已关闭。当前相关 suite 132 passed,Ruff、mypy、git diff --check 和 2-file 公共边界扫描通过。按 owner 明确指令,本 review 不等待最终 exact-head CI,也不在最新 main 上重跑 premerge canary;这两项 required validation 因而保持未验证。
语义与 CI 对齐
旧 head bdf1a1a8ec77b288070c7be58f694348aff4a7bb 上的同一 mutation 保持 identity 和 catalog readiness;修复 head 上三条 mutation/lifecycle 回归都 fail closed。没有新增状态词汇或 public protocol;复用现有 readiness identity。残余边界是完整 Python package/environment attestation不在本修复承诺内,POSIX 本地 symlink mutation 仍需最终 Windows CI 做平台补充。
我的整体评价
代码审查没有发现新的实现 blocker;这个 diff 把 #4817 建立的“isolated 且 doctor-verified runtime”补成一致的执行边界,机制小、owner 单一、回归能直接杀死原反例。但 capability review contract 要求 final CI 和最终 base/head canary,当前按 owner 指令跳过等待与重放,所以本次只能给出 REQUEST_CHANGES / validation hold,而不能把本地通过冒充 merge-ready approval。它修改 loopx/extensions/** runtime readiness,之后即使补齐门禁,也必须由独立 maintainer 合入。
English verdict: REQUEST_CHANGES - exact head 728d74abd9ad79ed911c8cf75155369cd451804d fixes the #4817 interpreter-identity blocker and passes 132 related tests, Ruff, mypy, diff hygiene, and public-boundary validation, but final exact-head CI and the final-base premerge canary remain unverified by explicit owner instruction; rerun those gates and re-review before an independent maintainer merge.
huangruiteng
left a comment
There was a problem hiding this comment.
Request changes conclusion (author-owned PR; GitHub blocks formal self-review)
动机
这个 PR 是 #4817 的 post-merge repair。#4817 把 extension-owned presentation validator 从 LoopX parent process 移进扩展自己的隔离 Python runtime,但 executable provider 的 doctor identity 只覆盖 launcher,没有覆盖后来真正执行 validator 的 Python。因此只替换 shebang/sibling Python、保持 provider wrapper 不变时,catalog 与 binding 仍会错误地认为 runtime 已经过 doctor。
本 PR 对那个精确反例做了正确修复:把 provider launcher digest 与 selected Python interpreter digest 合成一个 runtime identity,并让 install/doctor、catalog/binding stale check 继续复用同一持久化字段。没有新增第二份 readiness 状态,也没有扩大 extension 权限。
改动思路
_runtime_executable_identity 以 canonical JSON 组合 entrypoint_identity 与 python_interpreter_identity;resolve_runtime_entrypoint 对 shebang Python 和 opaque sibling Python 都返回该 composite。非 Python executable 仍沿用 launcher-only identity,python_module runtime 仍沿用既有 interpreter + module identity。
这条 owner 选择是对的:extension_doctor 在 probe 前后计算同一 identity,_verified_entrypoint 在 catalog/binding/presentation 使用前重算并比较,变化时走现有 doctor readiness is stale; run loopx extension doctor ... --execute 恢复路径。
但“实际执行 validator 的完整效果边界”仍没有被这份 composite 覆盖。隔离 Python 会根据 manifest 的 view_validator 动态 import extension site-packages 中的模块;该模块可以独立变化,而 launcher 和 Python binary 都不变。
具体改动
精确 head 728d74abd9ad79ed911c8cf75155369cd451804d 改 2 个文件、+123/-5。与上一轮审阅的 176bd0cd4f87d1e3d6fff580d1717eff80903e57 做 range-diff 后,PR patch 完全一致;旧 base 到新 base 在 loopx/extensions/**、tests/extensions/**、pyproject.toml、uv.lock 也没有变化,因此上一轮真实 venv 反例的前提没有失效。我仍在当前 head 上重跑了完整聚焦正向验证:
loopx/extensions/readiness.py:新增 35 行 composite identity 逻辑;tests/extensions/test_extension_runtime.py:新增 88 行,覆盖 opaque launcher、shebang interpreter mutation,以及真实 install → doctor → catalog/binding stale 生命周期。
关键代码讲解
-
_runtime_executable_identity只把 launcher 与 interpreter 两个 digest 放进 composite。它修复了此前的 interpreter-swap 缺口,但这里也是剩余 blocker 的位置:manifest 已声明的view_validatorimplementation 没进入 identity。 -
resolve_runtime_entrypoint返回同一个python_executable给 presentation 层执行,并把 interpreter bytes 纳入 readiness identity;路径/venv module tree 本身没有进入摘要。 -
_verified_entrypoint只要 active revision 与上述 composite 相等就返回 ready;因此它无法观察 site-packages validator module 的变化。 -
load_presentation_view_validator随后用这个 Python 执行-I -c,并动态 importview_validator。这证明 validator implementation 不是旁路数据,而是实际决定 projection 接受/拒绝的 effect artifact。
当前 head 的三个 focused suites 共 132 passed in 45.76s;Ruff、mypy 与 git diff --check 均通过。按 Goal 配置,本轮不咨询远端 CI。这些证据足以证明 interpreter mutation 修复生效,但没有覆盖下面的独立 module mutation。
对主干的风险
[P1] doctor identity 仍未绑定实际执行的 validator implementation
上一 exact head 的真实临时 venv 反例保持有效:provider wrapper、venv Python 路径与 Python binary 都不变,只把 site-packages 中 manifest 声明的 validator module 从返回 marker=a 改成返回 marker=changed-with-more-bytes。结果是:
install_verified = true;- mutation 前后
ResolvedRuntimeEntrypoint.identity完全相同; extension_status(...).doctor_verified前后都为true;- 隔离 validator 的实际输出已经变化。
该证据没有被机械继承:我先验证了当前 patch 与证据来源 head 完全相同,并检查了两个 base 之间没有改变 extension runtime 的调用者、依赖或测试前提。当前实现仍只读取 launcher 与 interpreter 两个工件,因此同一反例在当前 head 上仍成立。
也就是说,当前 patch 修复了“解释器被替换”,却仍允许“在同一解释器里执行的决策代码被替换”而不使 doctor receipt 失效。catalog/binding 会继续投影 ready,未经当前 doctor 验证的代码却拥有 projection acceptance authority。这正是 #4817 trust boundary 的同一 change reason,不是无关的增强要求。
最小修复不是另造 lifecycle 或 receipt,而是让现有 composite 同时绑定 manifest-declared view_validator 的实现工件(或能够完整代表该 versioned installed runtime 的 distribution/environment identity),并补一条真实回归:install → doctor → ready → 仅修改 validator module → catalog/binding/publish 在执行新 validator 前 fail stale;未修改 module 的对照仍应 ready。
我的整体评价
当前实现对上一轮明确指出的 interpreter-swap blocker 是有效且比例合适的修复,复用了正确的 owner,测试质量也不错;我不建议回退到 launcher-only identity,也不建议拆出新的 validator readiness 系统。
但 #4850 的目标是修复 #4817 的 doctor-verified validator runtime 边界。只绑定 Python binary、遗漏实际被 import 并执行的 validator implementation,仍会产生 false-ready。因此本 exact head 结论为 REQUEST_CHANGES。补齐同一 composite 与真实生命周期 mutation 后可以重新审查。
English verdict: REQUEST_CHANGES - exact head 728d74a; range-diff proves the patch is unchanged from the prior reviewed head, relevant base paths did not change, and 132 focused tests plus Ruff, mypy, and diff check pass, but the still-valid real-venv counterexample keeps identity and doctor_verified unchanged while the declared validator module changes behavior, so unverified decision-bearing code can still execute as ready.
Clean merge: main only added unrelated work. This sync removes the two stale-base failures seen at the previous head (test_settled_replay_construction protocol_action_packet expectations and test_blocked_transition_notice), both of which already pass on current main. Signed-off-by: LoopX Agent <agent@loopx.local> Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
A declared `presentation_surfaces[].view_validator` runs in the extension runtime interpreter, not in the LoopX process. The doctor identity bound the launcher and the selected interpreter, so replacing only the validator implementation left `ready` true and the catalog/binding path verified while a different decision code was executed on the next projection publish. Extend the composite runtime identity with the module file each declared extension-owned validator resolves to, resolved by the same `python -I` interpreter that runs the isolated validator. Only content that changed invalidates the proof, and a runtime that declares no extension-owned validator keeps its previous identity so unrelated extensions are not re-doctored. A validator the runtime interpreter cannot resolve is a fact about that declaration, not about the provider runtime: the marker records it in the identity while the doctor stays ready, and the projection surface that uses it reports its own `view_validator ... is unavailable` error. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
huangruiteng
left a comment
There was a problem hiding this comment.
Approval conclusion (author-owned PR; GitHub blocks formal self-approval)
动机
这个 PR 是 #4817 的 post-merge repair,并且它最终交付的边界比初始标题更宽。#4817 把 extension-owned presentation validator 从 LoopX parent process 移进扩展自己的隔离 Python runtime 之后,doctor-verified runtime identity 必须覆盖“实际执行的那段代码”,而不只是启动器。上一轮 exact head 728d74abd 的审阅已经用真实 venv 反例证明:只绑定 provider launcher + selected Python 仍然不够——在同一个解释器里替换 site-packages 中 manifest 声明的 validator module,ready 与 doctor_verified 都保持 true,而真正决定 projection 接收/拒绝的代码已经换掉了。
新 head 45d4b80ad7e08c1af35093602e94831cbbe36612 补上了这最后一环:把 manifest 声明的 extension-owned validator 实现工件纳入同一个 composite identity,并配备真实 venv 生命周期回归(install → doctor → ready → 只改 validator → catalog/binding/publish 在执行新 validator 之前 fail stale),以及未修改 module 的对照。因此这条 P1 已关闭,PR 现在覆盖 #4817 的完整 trust boundary。
改动思路
owner 选择没有变,仍然是 loopx/extensions/readiness.py 的单一 identity owner,没有新增第二份 readiness 状态、第二份 receipt、新的 CLI 或新的权限面:
declared_view_validators从已校验的 manifest 中选出 extension-owned validator 引用,并排除 LoopX 自有的 core validator(core validator 在 LoopX 进程内执行,不属于扩展工件,绑定它会让每次 LoopX 升级都作废所有安装)。_declared_validator_artifacts用和隔离执行完全相同的解释器与-I隔离解析每个声明 module 的源文件并绑定其内容摘要。这里刻意复用执行侧的解释器而不是 LoopX 自己的importlib,也不复用entrypoint_surface.module_source_path:后者解析的是 LoopX checkout 里的模块,而真实运行的是 runtime venv 里的安装副本(finance 测试里的.pthfixture 正好证明两者不同)。解析只做find_spec不 import:能解析但 import 期抛错的实现属于“使用处执行失败”,不是 provider runtime 的 identity 变化。_runtime_executable_identity只在声明了 extension-owned validator 时才把view_validator_artifacts加入 hash payload;没有该声明的 runtime 保持与旧实现逐字节相同的 identity,因此其它扩展不会被迫重新 doctor。_verified_entrypoint与extension_doctor现在传入同一份声明集合。修复过程中确实观察到“只有一侧传入声明集合”会让所有扩展被误判 stale,这条约束现在由测试固定。load_presentation_view_validator行为不变,只是与 identity owner 共享CORE_VIEW_VALIDATORS,避免两处对“哪些引用属于 LoopX”产生分歧。
具体改动
exact head 45d4b80ad7e08c1af35093602e94831cbbe36612 相对 origin/main 改 6 个文件、+523/-27:生产代码集中在 identity owner(loopx/extensions/readiness.py +181/-7)、stale check(loopx/extensions/runtime.py +5/-1)和一处常量/导入搬迁(loopx/extensions/presentation.py +6/-6);测试 +331/-13,覆盖 identity 声明绑定、真实 venv mutation 生命周期、声明变更与不可解析声明的处理。
关键代码讲解
declared_view_validators/_declared_validator_artifacts:声明集合是从 manifest 派生的确定集合(排序去重),工件映射为dict[str, str | None]。runtime 解释器无法解析的声明记录为None标记——它把“声明了什么”这件事纳入 identity 变化检测,同时不把健康的 provider runtime 误报成entrypoint_missing;真正的不可用仍由使用该 validator 的 projection surface 报view_validator ... is unavailable。解析结果按 (解释器, module) 记忆化,字节摘要每次重算,所以内容变化一定改变 identity。_runtime_executable_identity:composite 只在有 extension-owned validator 时扩展。没有该声明时旧 identity 与python_moduleruntime 的 payload 都不变,因此升级 LoopX 不会让无关扩展失效;有声明时才把 launcher + interpreter + validator 工件三者一起绑定。_verified_entrypoint:保持“只读、返回None”的风格,让各调用方继续持有自己的错误信息与恢复路径(doctor readiness is stale; run loopx extension doctor <id> --execute)。它与extension_doctor必须同参,这一点由新测试守住。load_presentation_view_validator:extension-owned 引用仍走隔离python -I -c,core 引用仍在本进程执行;搬迁的常量让 identity 的豁免集合与执行侧判断不可能不一致。- 回归测试:真实 venv +
.pth+ 真实子进程 + 真实 CLI 入口。对照组(未修改 module)里非法 view 仍被拒;只替换声明 module 后 catalogready变 false、publish 在执行 provider 之前抛 stale(invocation marker 不存在);重新 doctor 之后被替换的实现才真正执行,且落盘 view 里出现原本被拒的字段——证明执行的是新代码而不是旧 receipt。
验证
exact head 上的本地证据:tests/extensions 全量 980 passed(含同步 main 后的 lark 扩展用例);test_settled_replay_construction + test_blocked_transition_notice 20 passed;CI lint 作用域 ruff check tests loopx/canary loopx/control_plane loopx/domain_packs loopx/presentation 通过;mypy 通过;四个 extension smoke 通过;loopx canary premerge --from-git-diff --git-diff-base origin/main 为 passed(20 条命令,0 failure,0 warning,0 manual hold)。
另外做了两条针对本修复的关键证据:
- mutation check:把两个调用侧的声明集合暂时撤掉(还原修复前机制)后,finance 生命周期用例在
ready()应为 false 的断言处失败;恢复后通过。这隔离出“绑定 validator 工件”正是检测该 mutation 的机制。 - 真实本机状态对照(只读):用修复前 revision 与新 head 分别对同一份本机 extension state 计算 catalog readiness,7 个已安装扩展的 ready 向量完全一致(agent-runtime、lark 为 ready,其余 5 个各自因既有原因本来就不是 ready,包括本机 finance 扩展)。两个 ready 扩展仍与其已存 proof 匹配,说明 identity 未被无谓改动;对真实 finance runtime 重复计算 identity 也稳定。
对主干的风险
已关闭的 blocker(上一轮 P1):doctor identity 未绑定实际执行的 validator implementation。上一 head 728d74abd 的真实 venv 反例在新 head 上不再成立:只改 validator module 时 identity 变化、catalog ready=false、publish 在执行新 validator 之前失败,重新 doctor 后新实现才可运行。最小修复要求(同一 composite 绑定声明工件 + 真实 install → doctor → ready → mutation → fail stale 回归 + 未修改对照)已全部满足。
行为边界与兼容性:
- 声明了 extension-owned validator 的扩展在升级 LoopX 后需要一次
loopx extension doctor <id> --execute;没有该声明的扩展 identity 逐字节不变,真实状态对照已证明无额外失效。 - 新增的解析开销是“每个声明 module 每进程一次”的有界子进程(记忆化),不落在热路径上,也没有新增持久化字段、schema 或 CLI 面。
- 语义取舍已显式化:不可解析的声明不让 provider runtime fail-closed(避免把健康 runtime 报成
entrypoint_missing、也避免阻止 validator 稍后才安装的合法安装),而是记录在 identity 中并在使用处报错。
残余风险(本轮未验证,已记录):identity 绑定的是声明 module 的文件内容与声明集合,不含该 module 的传递依赖,也不代表完整的 distribution/environment identity——同字节但不同依赖、或路径变化而内容相同的替换仍可逃过摘要。这是 identity 既有粒度的有界限制,不是保留 false-ready 的理由;若将来需要更宽的边界,_declared_validator_artifacts 是唯一的扩展点。
本轮最强缺失证据:exact head 的远端 CI 在审阅时仍为 pending(5 条 check 未完成)。本结论建立在上述本地聚焦套件、extension smoke、风险预合并门禁与真实本机状态对照之上,并据此按维护者授权走 admin bypass 合并。
我的整体评价
修复方向正确、比例合适:没有另造 readiness 系统或新 receipt,而是在既有的单一 identity owner 上补齐真正执行的那段工件,并把“声明集合必须两侧一致”这个容易踩的坑用测试固定下来。回归证据从 mock 升级到真实 venv + 真实子进程 + 真实 CLI 入口,并给出未修改对照与修复前 mutation 失败证据,这足以支撑 #4850 现在覆盖 #4817 的完整 validator trust boundary。唯一遗留的是更宽 identity 粒度的残余风险与 pending 的远端 CI,均已在上文显式记录。
English verdict: APPROVE - exact head 45d4b80; the prior P1 is closed because the declared validator implementation is now resolved with the same isolated interpreter and bound into the existing composite runtime identity, with a real-venv install -> doctor -> ready -> mutate-only-validator -> fail-stale regression, an unmutated control, and an isolated mutation check showing the fix is the detecting mechanism; 980 extension tests, the replay/blocked-notice suites, CI-scope ruff, mypy, four extension smokes, and the pre-merge gate pass, and a read-only comparison over the seven locally installed extensions shows identical readiness verdicts except the intended new binding; residual risk is limited to identity granularity (no transitive imports or full environment identity) and pending remote CI.
The shipped-runtime guard requires every text-mode subprocess read in `loopx/**` to pin UTF-8 explicitly, because a locale-derived codec (gbk on a zh-CN host) would break the pipe reader and turn a successful child into a `None` stdout. The new declared-module resolver read its child in text mode without a codec; pin utf-8 on that call. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
huangruiteng
left a comment
There was a problem hiding this comment.
Approval conclusion (author-owned PR; GitHub blocks formal self-approval)
动机
这个 PR 是 #4817 的 post-merge repair,并且它最终交付的边界比初始标题更宽。#4817 把 extension-owned presentation validator 从 LoopX parent process 移进扩展自己的隔离 Python runtime 之后,doctor-verified runtime identity 必须覆盖“实际执行的那段代码”,而不只是启动器。上一轮 exact head 728d74abd 的审阅已经用真实 venv 反例证明:只绑定 provider launcher + selected Python 仍然不够——在同一个解释器里替换 site-packages 中 manifest 声明的 validator module,ready 与 doctor_verified 都保持 true,而真正决定 projection 接收/拒绝的代码已经换掉了。
新 head d5882ad6608cdc1ea363e0893362f796df99993f 补上了这最后一环:把 manifest 声明的 extension-owned validator 实现工件纳入同一个 composite identity,并配备真实 venv 生命周期回归(install → doctor → ready → 只改 validator → catalog/binding/publish 在执行新 validator 之前 fail stale),以及未修改 module 的对照。因此这条 P1 已关闭,PR 现在覆盖 #4817 的完整 trust boundary。
改动思路
owner 选择没有变,仍然是 loopx/extensions/readiness.py 的单一 identity owner,没有新增第二份 readiness 状态、第二份 receipt、新的 CLI 或新的权限面:
declared_view_validators从已校验的 manifest 中选出 extension-owned validator 引用,并排除 LoopX 自有的 core validator(core validator 在 LoopX 进程内执行,不属于扩展工件,绑定它会让每次 LoopX 升级都作废所有安装)。_declared_validator_artifacts用和隔离执行完全相同的解释器与-I隔离解析每个声明 module 的源文件并绑定其内容摘要。这里刻意复用执行侧的解释器而不是 LoopX 自己的importlib,也不复用entrypoint_surface.module_source_path:后者解析的是 LoopX checkout 里的模块,而真实运行的是 runtime venv 里的安装副本(finance 测试里的.pthfixture 正好证明两者不同)。解析只做find_spec不 import:能解析但 import 期抛错的实现属于“使用处执行失败”,不是 provider runtime 的 identity 变化。_runtime_executable_identity只在声明了 extension-owned validator 时才把view_validator_artifacts加入 hash payload;没有该声明的 runtime 保持与旧实现逐字节相同的 identity,因此其它扩展不会被迫重新 doctor。_verified_entrypoint与extension_doctor现在传入同一份声明集合。修复过程中确实观察到“只有一侧传入声明集合”会让所有扩展被误判 stale,这条约束现在由测试固定。load_presentation_view_validator行为不变,只是与 identity owner 共享CORE_VIEW_VALIDATORS,避免两处对“哪些引用属于 LoopX”产生分歧。
具体改动
exact head d5882ad6608cdc1ea363e0893362f796df99993f 相对 origin/main 改 6 个文件、+524/-27:生产代码集中在 identity owner(loopx/extensions/readiness.py +181/-7)、stale check(loopx/extensions/runtime.py +5/-1)和一处常量/导入搬迁(loopx/extensions/presentation.py +6/-6);测试 +331/-13,覆盖 identity 声明绑定、真实 venv mutation 生命周期、声明变更与不可解析声明的处理。
关键代码讲解
declared_view_validators/_declared_validator_artifacts:声明集合是从 manifest 派生的确定集合(排序去重),工件映射为dict[str, str | None]。runtime 解释器无法解析的声明记录为None标记——它把“声明了什么”这件事纳入 identity 变化检测,同时不把健康的 provider runtime 误报成entrypoint_missing;真正的不可用仍由使用该 validator 的 projection surface 报view_validator ... is unavailable。解析结果按 (解释器, module) 记忆化,字节摘要每次重算,所以内容变化一定改变 identity。_runtime_executable_identity:composite 只在有 extension-owned validator 时扩展。没有该声明时旧 identity 与python_moduleruntime 的 payload 都不变,因此升级 LoopX 不会让无关扩展失效;有声明时才把 launcher + interpreter + validator 工件三者一起绑定。_verified_entrypoint:保持“只读、返回None”的风格,让各调用方继续持有自己的错误信息与恢复路径(doctor readiness is stale; run loopx extension doctor <id> --execute)。它与extension_doctor必须同参,这一点由新测试守住。load_presentation_view_validator:extension-owned 引用仍走隔离python -I -c,core 引用仍在本进程执行;搬迁的常量让 identity 的豁免集合与执行侧判断不可能不一致。- 回归测试:真实 venv +
.pth+ 真实子进程 + 真实 CLI 入口。对照组(未修改 module)里非法 view 仍被拒;只替换声明 module 后 catalogready变 false、publish 在执行 provider 之前抛 stale(invocation marker 不存在);重新 doctor 之后被替换的实现才真正执行,且落盘 view 里出现原本被拒的字段——证明执行的是新代码而不是旧 receipt。 - 远端 CI 反馈的修复:上一 head
45d4b80ad的 shard 3 报出test_shipped_runtime_pins_utf8_for_every_text_mode_subprocess_call失败,指认新增解析调用的loopx/extensions/readiness.py:194以text=True读取子进程却未固定编码(locale 派生的 gbk 会把成功的子进程读成stdout=None)。本 head 在该调用上固定encoding="utf-8",guard 套件 4 passed。
验证
exact head 上的本地证据:tests/extensions 全量 980 passed(含同步 main 后的 lark 扩展用例);test_settled_replay_construction + test_blocked_transition_notice 20 passed;CI lint 作用域 ruff check tests loopx/canary loopx/control_plane loopx/domain_packs loopx/presentation 通过;mypy 通过;四个 extension smoke 通过;loopx canary premerge --from-git-diff --git-diff-base origin/main 为 passed(20 条命令,0 failure,0 warning,0 manual hold)。
针对运行期跨切面契约,另外补跑了 shipped-runtime UTF-8 guard 与九个根级 guard 套件(tests/test_runtime_subprocess_utf8.py、skill discovery、nokv shadow provider probes、manager channel/context、turn journal/codex cli/driver、kiro host surface):212 passed;tests/extensions 与 UTF-8 guard 合并运行 984 passed。
另外做了两条针对本修复的关键证据:
- mutation check:把两个调用侧的声明集合暂时撤掉(还原修复前机制)后,finance 生命周期用例在
ready()应为 false 的断言处失败;恢复后通过。这隔离出“绑定 validator 工件”正是检测该 mutation 的机制。 - 真实本机状态对照(只读):用修复前 revision 与新 head 分别对同一份本机 extension state 计算 catalog readiness,7 个已安装扩展的 ready 向量完全一致(agent-runtime、lark 为 ready,其余 5 个各自因既有原因本来就不是 ready,包括本机 finance 扩展)。两个 ready 扩展仍与其已存 proof 匹配,说明 identity 未被无谓改动;对真实 finance runtime 重复计算 identity 也稳定。
对主干的风险
已关闭的 blocker(上一轮 P1):doctor identity 未绑定实际执行的 validator implementation。上一 head 728d74abd 的真实 venv 反例在新 head 上不再成立:只改 validator module 时 identity 变化、catalog ready=false、publish 在执行新 validator 之前失败,重新 doctor 后新实现才可运行。最小修复要求(同一 composite 绑定声明工件 + 真实 install → doctor → ready → mutation → fail stale 回归 + 未修改对照)已全部满足。
行为边界与兼容性:
- 声明了 extension-owned validator 的扩展在升级 LoopX 后需要一次
loopx extension doctor <id> --execute;没有该声明的扩展 identity 逐字节不变,真实状态对照已证明无额外失效。 - 新增的解析开销是“每个声明 module 每进程一次”的有界子进程(记忆化),不落在热路径上,也没有新增持久化字段、schema 或 CLI 面。
- 语义取舍已显式化:不可解析的声明不让 provider runtime fail-closed(避免把健康 runtime 报成
entrypoint_missing、也避免阻止 validator 稍后才安装的合法安装),而是记录在 identity 中并在使用处报错。
残余风险(本轮未验证,已记录):identity 绑定的是声明 module 的文件内容与声明集合,不含该 module 的传递依赖,也不代表完整的 distribution/environment identity——同字节但不同依赖、或路径变化而内容相同的替换仍可逃过摘要。这是 identity 既有粒度的有界限制,不是保留 false-ready 的理由;若将来需要更宽的边界,_declared_validator_artifacts 是唯一的扩展点。
远端 CI 现状:上一 head 45d4b80ad 上出现的三处红:test_shipped_runtime_pins_utf8_for_every_text_mode_subprocess_call 是本 PR 自己的问题,已在本 head 修复;test-shard (2) 里的 test_project_lifecycle_goal_channel.py::test_refresh_state_dispatches_and_replays_post_writeback_sidecar 与 test_turn_machine_credential.py::test_two_goal_runtimes_use_machine_credential_for_plan_and_dispatch,以及 node-minimum-compatibility 的 SQLite 资格问题,都已在 main 自身最近一次 CI(head 3de02368a)和 main 代码的本地复现中确认是既有失败,与本 diff 无关;shard 归属变化只是因为本 PR 新增了测试。本 head 的远端 CI 在审阅时仍在运行。结论建立在上述本地聚焦套件、根级 guard 套件、extension smoke、风险预合并门禁与真实本机状态对照之上,并据此按维护者显式授权走 admin bypass 合并。
我的整体评价
修复方向正确、比例合适:没有另造 readiness 系统或新 receipt,而是在既有的单一 identity owner 上补齐真正执行的那段工件,并把“声明集合必须两侧一致”这个容易踩的坑用测试固定下来。回归证据从 mock 升级到真实 venv + 真实子进程 + 真实 CLI 入口,并给出未修改对照与修复前 mutation 失败证据,这足以支撑 #4850 现在覆盖 #4817 的完整 validator trust boundary。唯一遗留的是更宽 identity 粒度的残余风险与 pending 的远端 CI,均已在上文显式记录。
English verdict: APPROVE - exact head d5882ad; the prior P1 is closed because the declared validator implementation is now resolved with the same isolated interpreter and bound into the existing composite runtime identity, backed by a real-venv install -> doctor -> ready -> mutate-only-validator -> fail-stale regression, an unmutated control, and a mutation check showing the binding is the detecting mechanism; 984 extension-plus-codec-guard tests, 212 root-level guard tests, the replay/blocked-notice suites, CI-scope ruff, mypy, four extension smokes, and the pre-merge gate pass, a read-only comparison over the seven locally installed extensions shows identical readiness verdicts apart from the intended new binding, and the UTF-8 codec failure remote CI found on the previous head is fixed in this head; residual risk is identity granularity (no transitive imports or full environment identity) plus concurrently running remote CI, whose two test-shard failures and node-minimum failure are reproduced on main itself and are pre-existing.
Summary
Follow up on #4817 by binding the extension runtime's decision code — not only its launcher — into the doctor-verified runtime identity.
The merged implementation correctly moved extension validators out of the LoopX parent process. The remaining hole was narrower and deeper than the first revision of this PR assumed:
view_validatorimplementation inside the same interpreter and venv was still invisible, soreadyanddoctor_verifiedstayed true while different decision code decided projection acceptance.This head closes the second case inside the same identity owner.
Changes
-Iisolation the isolated validator runs with, memoizing the resolved path while re-reading the bytes on every computation;view_validator ... is unavailable;Validation
pytest -q tests/extensions— 980 passed (after syncingmain, including the new lark extension suites)pytest -q tests/control_plane/test_settled_replay_construction.py tests/control_plane/test_blocked_transition_notice.py— 20 passedruff check tests loopx/canary loopx/control_plane loopx/domain_packs loopx/presentation— passed (the CI lint scope)mypy— passedpython examples/extension-entrypoint-surface-smoke.pyplus three sibling extension smokes — passedloopx canary premerge --from-git-diff --git-diff-base origin/main—passed(20 commands, 0 failures, 0 warnings, 0 manual holds)ready()assertion, which isolates the artifact binding as the detecting mechanism.Merge boundary
This changes
loopx/extensions/**runtime readiness semantics. Repository policy requires exact-head review, and the exact head carries a published approval conclusion review generated from the review packet. The merge itself uses admin bypass under explicit maintainer authorization.Closes the post-merge blocker recorded on #4817.