fix(extensions): isolate projection validator runtime - #4817
Conversation
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.
动机
这个 PR 要修复 extension presentation validator 的真实运行边界:validator 属于 extension,就应该在 doctor 验证过的 extension runtime 里解析依赖,而不是被 import 进 LoopX parent process。否则扩展在自己的 venv 中可用,发布 projection 时却可能因为父环境缺包而失败,或者意外使用父环境中的同名包。
这个目标和边界是合理的。受影响的是声明了 view_validator 的 extension projection publication;core validator 仍保持进程内执行,未声明 validator 或未启用的 extension 不进入新路径,projection schema、Dashboard/Lark 表现以及安装权限都没有改变。
改动思路
实现复用了现有 ResolvedRuntimeEntrypoint、doctor readiness 与 run_capped_process:readiness 从 POSIX shebang 或 opaque/Windows-style launcher 的 sibling venv Python 解析真正解释器;presentation 用该解释器执行 python -I -c validator shim,移除 ambient PYTHONPATH,并限制 stdout/stderr、超时和响应 shape。发布前后继续比较 doctor-verified entrypoint identity/revision,以避免已验证 runtime 漂移。
这条架构链路的关键契约是:“实际执行 validator 的全部 executable artifact,必须就是 doctor identity 所验证的 artifact”。当前实现完成了 runtime 选择和进程隔离,但 identity 只覆盖其中一部分。
具体改动
readiness.py新增_python_executable_for_script,覆盖 shebang launcher 与 sibling Python,resolve_runtime_entrypoint返回 launcher identity 和python_executable。presentation.py的 extension validator 从 parent import 改为隔离 subprocess,保持 core validator 原路径,并对进程结果做 cap/schema/error 处理。- extension presentation、finance provider 和 runtime tests 覆盖正常执行、缺失依赖、异常响应、Windows/opaque launcher 等情况。
关键代码讲解
_python_executable_for_script(readiness.py:25)负责决定实际执行的 Python;resolve_runtime_entrypoint(readiness.py:143)把它放进 ResolvedRuntimeEntrypoint.python_executable。但是同一构造在 readiness.py:151 附近仍把 identity 设为 launcher 的 _file_identity,没有纳入 interpreter digest。
load_presentation_view_validator(presentation.py:325)随后确实使用 python_executable 执行隔离 shim;publish_extension_projection(约 presentation.py:744)在发布前后的 stale check 只比较 .identity。因此 doctor 检查的是 launcher A,而执行链实际信任的是 launcher A + interpreter B;B 可以独立变化却不使 readiness 失效。
我在 exact head 上用真实临时 executable/symlink 做了 mutation:固定 opaque provider launcher,先把 sibling python 指向当前 sys.executable,再替换为 /bin/sh。两次 resolve_runtime_entrypoint 返回了相同 identity;也就是说,在 launcher bytes 不变时,doctor 仍会认为 runtime 已验证,而 validator 将通过另一个 interpreter artifact 执行。
对主干的风险
[P1] doctor identity 没有覆盖实际执行 validator 的 interpreter
触发条件并不要求绕过 manifest 或修改 launcher:只需 doctor 之后 sibling venv Python 被升级、替换或重建,而 entrypoint wrapper 保持不变。当前 stale check 会通过,新的 interpreter 获得接受/拒绝 projection 的执行权,但没有对应 doctor receipt。这个缺口会让本 PR 想建立的“extension-owned、doctor-verified runtime”只做到隔离,没有做到完整的 verified binding。
最小修复建议:把 interpreter artifact identity(必要时再加 validator module/runtime environment identity)合入 ResolvedRuntimeEntrypoint.identity 与 doctor revision,或者在每次 validator 执行前单独校验 exact interpreter identity;发现变化必须 fail stale 并要求重新 doctor。回归应只替换 sibling interpreter,断言 identity/revision 变化或发布被拒绝,同时保留 unchanged runtime 正常通过。
语义与 CI 对齐
exact-head 本地验证:三个 focused suite 共 129 tests 通过;Ruff、compileall、extension-presentation-surface-smoke.py 和 git diff --check 均通过。上述 interpreter-swap mutation 按预期失败并暴露缺口。远端 CI 不是这个结论的替代:即使其余 checks 转绿,也不会覆盖 doctor 后 interpreter artifact 独立变化的反例。
我的整体评价
隔离 extension validator、移除 parent PYTHONPATH、限制 subprocess 输出,并复用既有 doctor/presentation owner,是正确且比例合适的改动;没有必要再造 validator service,也不需要 frontend/Lark companion。未来向的 bounded refactor 应聚焦为一个 composite runtime identity,由 doctor 和 execution 共用。当前 blocker 修复后,这个 PR 会形成完整、可回滚且清晰的 extension runtime trust boundary。
English verdict: REQUEST_CHANGES - head e47401a; the doctor identity hashes the launcher but omits the interpreter that actually executes the isolated validator, so an interpreter swap remains verified. 129 focused tests, Ruff, compileall, the public presentation smoke, and diff check passed; the exact-head interpreter-swap mutation failed as expected and exposes the blocker.
|
Exact-head CI attribution for
The bounded repair successor already exists as #4819. Its exact head has all shards, |
|
CI attribution follow-through is complete:
The next decision should use #4817's new exact-head results. Any fresh failure should be attributed from that head before changing production code. |
huangruiteng
left a comment
There was a problem hiding this comment.
Request changes conclusion (author-owned PR; GitHub blocks formal self-review)
Exact head reviewed: bdf1a1a8ec77b288070c7be58f694348aff4a7bb
动机
这个 PR 要解决的核心问题是:扩展声明的 presentation validator 应当在扩展自己的运行时中执行,而不是借用 LoopX 父进程的 Python import 环境。这个方向正确,也补上了 publish、持久化 readback、catalog status/details 对同一 validator 语义的复用;内置 opaque validator 仍保留进程内路径,边界总体清楚。
改动思路
实现把 runtime entrypoint 解析为 ResolvedRuntimeEntrypoint,同时记录启动前缀、PATH 前缀、身份摘要和实际 Python executable。扩展 validator 通过隔离解释器、-I 和裁剪后的环境进入 capped subprocess;读取、发布和 catalog 投影都复用同一加载逻辑。对于 Python module runtime,identity 已组合解释器与模块工件;对于 executable runtime,则先解析 provider launcher,再从 shebang 或同目录的 Python launcher 推导 validator interpreter。
具体改动
resolve_runtime_entrypoint()统一解析 module runtime 与 executable runtime,并将选中的解释器传给 presentation validator。load_presentation_view_validator()为扩展声明的 validator 建立隔离子进程协议,限制响应大小、移除 ambientPYTHONPATH,并把无法解析、不可调用和运行时拒绝区分开。- publish、readback、catalog status/details 共享这条验证路径;测试覆盖 POSIX shebang、Windows/opaque launcher、默认关闭隔离、无效输出与真实安装/doctor 生命周期。
关键代码讲解
resolved_entrypoint_identity():解析并哈希 operator 选择的 provider launcher,保留原 launcher 所在目录供同目录工具查找。_python_executable_for_script():从脚本 shebang 或同目录 launcher 推导真正执行 validator 的 Python。resolve_runtime_entrypoint():把 launcher identity 与推导出的 Python executable 聚合为运行时解析结果;本次阻塞点就在 executable 分支二者没有共同进入 identity。load_presentation_view_validator():实际使用python_executable启动隔离 validator,因此它是 doctor 必须绑定的效果边界。_verified_entrypoint()/extension_catalog_entries():用已记录的 doctor identity 判定扩展是否仍 ready,并把结果投影到 catalog。
对主干的风险
[P1] doctor identity 未绑定实际执行 validator 的 interpreter
loopx/extensions/readiness.py:148-155 的 executable runtime 分支把 identity 仅设为 provider launcher 的摘要,但 python_executable 是随后从 shebang 或 sibling launcher 单独推导的。实际执行 validator 的解释器因此不在 doctor 已验证身份内。provider wrapper 不变、解释器工件被替换后,_verified_entrypoint() 仍会认为 doctor 结果有效,catalog 也继续投影为 ready,随后 validator 却在未经 doctor 验证的新解释器上运行。
我用两层变异验证复现了这一点:第一层直接保持 provider identity 与 Python launcher path 不变,只把解释器目标从原 Python 换成 /bin/sh,verified identity 仍完全相同;第二层走真实的 install → doctor → catalog 生命周期,在 doctor 已 verified 后替换 interpreter,catalog 的 ready 仍从 true 保持为 true。这不是 mock 决策造成的假阳性,而是当前持久化 readiness 合约遗漏了实际效果工件。
建议的最小修复是:executable runtime 的 composite identity 同时绑定 provider launcher 和最终选中的 interpreter artifact(包括 symlink 最终目标/内容身份),使任一工件变化都让既有 doctor receipt 失效;再保留一条真实 install/doctor/catalog 生命周期回归,证明 interpreter 变更后必须重新 doctor。只绑定字符串路径不够,因为上述复现正是在路径不变时替换目标。
语义与 CI 对齐
本地精确头验证结果:三个相关测试文件共 129 passed;Ruff、compileall、公开的 extension presentation surface smoke、git diff --check 均通过;基于最终 diff 的 premerge canary 选择 10 项并以 10/10 通过,无 manual hold。按当前 capability 的 wait_for_ci=false 策略,本次没有读取或等待远端 CI,因此不把远端状态作为结论证据。
旧 head 上提出的同一 blocker 并未在当前 head 消失:当前更新主要是合入 main,修复的是另一条无关 CI 路径;上述真实生命周期变异仍在精确 head bdf1a1a8ec77b288070c7be58f694348aff4a7bb 复现。
我的整体评价
隔离 validator runtime 的产品方向、子进程边界和正常/失败路径覆盖都扎实,且本次改动没有把扩展 authority 提升到 LoopX 主进程。不过 doctor/readiness 的职责正是保证稍后执行的运行时与已验证工件一致;当前遗漏 interpreter 会让安全边界在 catalog 显示 ready 时悄然漂移,因此在 composite identity 与真实生命周期回归补齐前不能批准。未来向前看,本次最合适的相关收敛就是让“将要执行的全部工件”拥有一个 typed composite identity;不需要再引入新的通用 runtime 框架。
English verdict: REQUEST_CHANGES - exact head bdf1a1a still leaves the validator interpreter outside the doctor-verified runtime identity; 129 focused tests and 10/10 premerge canaries pass, but a real install/doctor lifecycle mutation remains ready after the interpreter changes to /bin/sh.
|
Post-merge self-repair: this PR was merged at |
Summary
PYTHONPATHremovedMotivation
An installed, enabled, doctor-ready extension could still fail
publish-projectionwhen its validator package was not importable by the parent LoopX process. The provider runtime was authoritative for execution, but validator loading accidentally depended on the caller environment.Validation
uv run --extra test python -m pytest -q tests/extensions/test_extension_presentation.py tests/extensions/test_finance_value_discovery_extension.py tests/extensions/test_extension_runtime.py(129 passed)uv run --extra test ruff check ...(passed)uv run --extra test python examples/extension-presentation-surface-smoke.py(passed before the exact-head refinement; its covered path is included in the 129-test rerun)uv run --extra test python -m compileall -q loopx/extensions/readiness.py loopx/extensions/presentation.py(passed)loopx checkon the original four changed files (public-boundary scan clean; unrelated existing registry warnings only)view_validator ... unavailable; exact head published and verified exact readback without ambientPYTHONPATHPYTHONPATHabsentProduct surface
CLI publish/readback behavior is fixed. No frontend or Lark companion change is needed: both already consume the same persisted projection envelope; this patch changes only how Core validates that envelope against the active extension runtime.
Boundaries
No credentials, private Goal state, raw provider output, generated lockfile, or local paths are included. This PR changes a control-plane/runtime boundary and is intentionally left for maintainer review rather than self-merge.