fix(update): qualify activation from an immutable source commit - #4460
Conversation
`loopx update check --ref <sha>` reported
activation_qualification_required
trusted installed-versus-target source lineage is unavailable
for an install that the same pinned SHA had just produced. The qualification
read `freshness_source_git_commit` from `loopx doctor`, which only branch refs
populate, so a full-commit install could never prove itself active even though
the installer had genuinely installed that commit.
A full commit SHA is its own commit identity, so the qualification now resolves
the selected immutable ref to its own commit, compares it with the installed
manifest source commit, and reports `runtime_active` on a match. A mismatch
stays `activation_qualification_required` with an explicit
"installed source commit does not match the selected immutable source commit"
reason instead of the generic lineage message.
Move the qualification into `loopx/activation_qualification.py` while changing
it: `loopx/self_update.py` was at 1485 lines with this fix applied against the
1500-line maintainability ratchet, and this module owns the one decision the
change touches. `loopx/self_update.py` returns to 1399 lines and keeps the
schema constant and the not-applicable stub through an explicit import;
`loopx/semantics/inventory_v0.json` regenerates to `source_files: 1177`.
Disclosure: pinned-commit installs now qualify as `runtime_active` where they
previously failed closed; branch-ref behavior is unchanged. Docs in
`docs/product/release-readiness.md` and `docs/guides/installing-loopx.md` state
the new receipt, and `examples/loopx-update-smoke.py` covers the matching and
mismatching immutable cases.
Validation: 65 passed / 1 skipped across
`tests/test_self_update_runtime_activation.py`,
`tests/test_archive_installer_commit_response.py`,
`tests/test_doctor_install_freshness.py`, and
`tests/canary/test_maintainability_ratchet.py`;
`loopx canary premerge --from-git-diff --timeout-seconds 300` gate=passed with
19 executed checks, 0 failures, and a clean public/private boundary scan.
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
298caca to
f5d2969
Compare
huangruiteng
left a comment
There was a problem hiding this comment.
Approval conclusion (author-owned PR; GitHub blocks formal self-approval)
Reviewed exact head f5d296926c68f6b5053acae158aff26ae4359d3e (rebased onto 2e310d768 after #4458 and #4456 merged). Evidence was executed at this head, not inherited from the earlier 298cacaee head.
动机
loopx update check --ref <40 位 SHA> 对本机这类「按固定 commit 安装」的运行时,永远给不出激活结论。qualification 读取的是 loopx doctor 的 freshness_source_git_commit / manifest_source_freshness_relation,而这两个字段只在 ref 能解析成分支血缘时才有值;用完整 SHA 安装时 target_source_commit 始终是 None,于是落进兜底分支,返回 activation_qualification_required,理由是 trusted installed-versus-target source lineage is unavailable。
这条路径上的最小修复并不小:直接放宽成「manifest 里有 commit 就算激活」会同时接受 manifest ref 指向另一个 commit 的安装,等于削弱身份校验;而把改动留在 loopx/self_update.py 里,文件会到 1515 行,直接撞上仓库自己 1500 行的可维护性棘轮。触发方是真实生产入口:本机管家运行时(release 20260915T144512Z,由 main@9719dc0d4 固定安装)在安装成功后仍然报告「未证明激活」,而这条回执正是 agent 敢不敢声称「这个修复已在运行时生效」的依据。
改动思路
入口是 loopx update check / loopx update plan → build_update_plan,权威输入是 doctor 的 install_freshness(含 manifest_source_repo/ref/git_commit)加上本次选中的 repo/ref。本次把「已安装运行时是否证明选中更新源已激活」这一个判定整体搬进新模块 loopx/activation_qualification.py,由一个函数 runtime_activation_qualification 独占,loopx/self_update.py 只负责计划与呈现。判定边界没有扩大:既有五个分支的优先级、source_identity_matches 先于任何血缘比较、以及所有 fail-closed 结果都原样保留。
对「新引入的状态」这里其实没有任何新状态:decision / runtime_active / successor 仍是 loopx_runtime_activation_qualification_v0 上的既有封闭集合,唯一变化是完整 SHA 这种 ref 会把自身作为 target commit 参与比较,而真正被比较的权威事实仍是已安装 manifest 的 commit。抽出的 105 行是逐字搬移,新增逻辑约 33 行。复用上是「拆出独立 owner」而不是新增并列实现:rg 在 head 上已经找不到任何 _runtime_activation_qualification 的定义或调用者,loox/runtime_activation.py(#4458 刚合并)负责的是安装后的服务重启,与这里不重叠。
具体改动
关键代码讲解
loopx/activation_qualification.py:34 runtime_activation_qualification —— 判定表的唯一 owner。新增的前置步骤只有一处:selected_commit and not target_commit 时,把完整 SHA 作为 target commit,并在与已安装 commit 相同时把关系置为 same。只有当 doctor 没有血缘时才走这一步,所以分支 ref 的既有行为一点没动。分支顺序保持:source_identity_matches 仍然最先裁决,其次是 runtime_active 与 installed_behind / diverged,新增的 elif has_commit_pair and selected_commit 只负责「commit 对得上身份但对不上目标」这一种以前不存在表达方式的组合。
loopx/activation_qualification.py:21 _immutable_source_commit —— 私有判定,要求 ^[0-9a-fA-F]{40}$ 全匹配才视为不可变 ref,并统一小写。分支名、tag、短 SHA 一律返回 None,新路径整体失效。它的存在让「ref 即 commit 身份」这件事有唯一落点,而不是散落在调用方。
loopx/self_update.py build_update_plan —— 只做导入替换:从新模块引入 runtime_activation_qualification 与 RUNTIME_ACTIVATION_QUALIFICATION_SCHEMA_VERSION,not_applicable 兜底仍由同一常量渲染,schema id 与 payload 键保持不变。文件从 1485 行降到 1399 行,棘轮告警被消除而不是新增。
examples/loopx-update-smoke.py:449 test_immutable_source_ref_qualifies_activation —— 用一份 freshness_source_git_commit=None、manifest_source_ref=<SHA> 的 doctor 样本,同时钉住匹配(runtime_active、revision_relation=same、无需 successor)与不匹配(activation_qualification_required 且理由点名 commit 差异)两种回执;旧代码在匹配样本上就会失败。
tests/test_self_update_runtime_activation.py —— 两个新单测分别覆盖「无需远端血缘即解析出 target commit」与「commit 不一致时报出差异」,与既有 test_missing_commit_lineage_never_proves_runtime_active、test_different_selected_source_never_reuses_unrelated_lineage 一起锁住全部拒绝分支。
文档侧在 docs/product/release-readiness.md(Merged Is Not Runtime-Active)与 docs/guides/installing-loopx.md(安装/更新检查)各补一段,说明固定 commit 安装直接按 manifest commit 判定;loopx/semantics/inventory_v0.json 按生成器重新生成到 source_files: 1177。
对主干的风险
最强回归场景是「40 位 hex 的 ref 却不是已安装 commit」,或被误当成 commit 的分支/tag 名。触发条件是 source.ref 匹配 40 位 hex 且 doctor 没有血缘;代码路径上,新分支在 source_identity_matches 与 runtime_active/installed_behind/diverged 之后才可达,最坏结果仍是 fail-closed 的 activation_qualification_required,而不会把未证明的安装升格。若真的出现 40 位 hex 的分支名,最小修复是优先采用 doctor 血缘,而代码已经是这个次序。
可观测面是完整的:decision、.reason、.target_source_commit、.revision_relation、successor 都在 JSON 回执里,markdown 渲染器也会显示 successor 与推荐动作。回滚就是 loopx update --rollback <release-id>,本改动不留持久状态。验证上,同一份合成输入在基线与 head 各跑一次真实公共入口:基线返回 activation_qualification_required + 通用血缘理由 + 「未证明激活」,head 返回 runtime_active / revision_relation=same / 无需 successor;本机已安装运行时也复现了修复前的旧回执。仓库本地门禁 loopx canary premerge --from-git-diff 全绿(19 项、0 失败、改动文件公开/私有边界扫描干净),pytest 65 passed / 1 skipped(Windows-only skip),update smoke、棘轮、语义词汇漂移与 ruff check 均通过。尚未验证的是「装一个由本 commit 构建的 release 后再读一次真实回执」,以及本 head 的远端 CI(复审时仍在跑,合并前必须为绿)。
我的整体评价
在只比较变更代码、不把「全绿」当论证的前提下,这是比例合适的修复:用户可见问题是「固定 commit 安装永远无法自证激活」,机制成本是一个 138 行、单一公共入口的模块加约 33 行解析逻辑,没有新依赖、新 CLI 入口、新配置、新持久状态,反而消掉了一个既有的棘轮违规。基线/head 对比显示只有两条不可变 ref 的行为改变,分支 ref、发散的 ref、跨源选择三类拒绝路径逐字保持不变。类型语义上没有新增枚举值,措辞保持领域中立,判定仍然是机器可读的 decision + successor.required 而不是散文建议;公开协议命名没有暗示比实现更大的权限生命周期(它连写权限都没有)。剩余风险已在上文列明:新 head 的远端 CI 与「安装后再读一次真实回执」。两者之一未落地前,我不会把它当作已激活结论;对于复审,我需要的就是这两项在同一个 head 上的证据。
English verdict: APPROVE at exact head f5d296926c68f6b5053acae158aff26ae4359d3e. A pinned-commit install previously could never satisfy the activation qualification because the target commit was only derivable from branch lineage; the change resolves a full 40-hex ref to its own commit inside the qualification that already owns the decision, keeping every rejection branch fail-closed, and moves that boundary into a 138-line loopx/activation_qualification.py so loopx/self_update.py (1485 → 1399) clears the repository's 1500-line ratchet. Validation: pytest 65 passed / 1 skipped, loopx-update-smoke ok, loopx canary premerge --from-git-diff gate passed (19 checks, 0 failures, clean boundary scan), semantic inventory current at 1177 source files, and a baseline/head probe of the same fixture showing activation_qualification_required → runtime_active with revision_relation=same. No blocking finding; the only open items are remote CI on this exact head and a post-install live readback, neither of which is claimed as evidence here.
后续(非阻塞):
|
Summary
loopx update check --ref <40-hex-commit>reportedfor an install that the same pinned SHA had just produced. The qualification read
freshness_source_git_commitfromloopx doctor, which only branch refs populate, so afull-commit install could never prove itself active even though
scripts/install.shhadgenuinely installed that commit. On this host the pinned-
mainsteward runtime(
manifest_source_git_commit=…9719dc0d,kind=github_archive,archive_sha256present)kept reporting an unproven activation after a successful install.
Behavior
A full commit SHA is its own commit identity, so the qualification now resolves the
selected immutable ref to that commit and compares it with the installed manifest source
commit:
runtime_active,revision_relation=same,successor.required=false;activation_qualification_requiredwith an explicitinstalled source commit does not match the selected immutable source commitreasoninstead of the generic lineage message.
Branch-ref behavior is unchanged, including the existing fail-closed cases for missing
lineage, mismatched
repo/ref, andinstalled_behind/diverged.Bounded refactor
loopx/self_update.pywas at 1515 lines against the 1500-line maintainability ratchetafter the fix. The touched boundary — one decision about whether the installed runtime
proves the selected update source is active — moved to
loopx/activation_qualification.py.loopx/self_update.pyreturns to 1394 lines andkeeps the schema constant and the not-applicable stub through an explicit import.
loopx/semantics/inventory_v0.jsonregenerates tosource_files: 1176.Validation
pytest tests/test_self_update_runtime_activation.py tests/test_archive_installer_commit_response.py tests/test_doctor_install_freshness.py tests/canary/test_maintainability_ratchet.py -q→ 63 passed, 1 skippedloopx canary premerge --from-git-diff --timeout-seconds 300→gate.status=passed, 19 executed checks, 0 failures, public/private boundary scan clean (7 files)examples/loopx-update-smoke.pycovers the matching and mismatching immutable cases; the ratchet and semantic-vocabulary-drift smokes passDisclosure
Pinned-commit installs now qualify as
runtime_activewhere they previously failedclosed.
docs/product/release-readiness.mdanddocs/guides/installing-loopx.mdstatethe new receipt.