[REQ-128][CODE] 切换 BundleGraph V3 ledger、claim set 与全部 writer - #757
Conversation
review #757 Major。用户对一个「只属于某个 Bundle、自己从没单装过」的条目点删除, 界面显示「已移除」,而文件、record、图、claim 一件没动 —— 刷新后条目依旧。 链路:`package-admission` 的 claimMutations 只 acquire 一个 `bundle:` owner, standalone owner 要等用户自己再单装一次才出现。而 `directUninstallVerdict` 只看 「有没有 Bundle owner」就选 `release-claim-only`,不问那份 standalone claim 在不在; `releaseStandaloneClaim` 对不存在的 owner 仍无条件提交一次写并回 ok;planner 回 `ok:true`;Hub 只看 `ok` ⇒ 显示「已移除」。 修法(判决在写盘之前,三处独立成闸): - `directUninstallVerdict` 拆出第三支 `refuse` —— 有 Bundle owner 但没有 standalone owner = 没有可释放的东西,响亮拒绝; - `planDirectUninstall` 与 `removeRecordV2` 各自消费 `refuse`(后者若漏,去账会留下 指向不存在 record 的 dangling claim); - `releaseStandaloneClaim` 自带前置:没有可释放的 owner 就在写盘之前失败,不依赖 调用方先做对判决。 「双 owner 时只释放 standalone」的行为原样保留。 闸:`ext-package-ledger-uninstall.test.ts` 新增 bundle-only 组(5 种 kind × ok:false + 零 rename + 字节零改动 + 零 installer 调用 + 实物仍在 + claim 原样),以及 `releaseStandaloneClaim` 直呼拒绝;`ext-package-ledger-v3.test.ts` 补 verdict 层 bundle-only 判决(违规形状不放集合首位)。修复前这些全红,实测 planner 回 `ok:true` + 一次 rename。 更正上一轮报告:那里写「`release-claim-only` 分支保证 claim 必然存在,所以 `releaseStandaloneClaim` 的无条件写盘不可达」—— 该前提为假,fresh package 安装后 只有 Bundle owner。因此 legacy-protected 直呼 `releaseStandaloneClaim` 的语义 一并从「静默成功的空操作」改为「响亮拒绝且零落盘」(生产从不走这条,保护不变)。 Refs #706
自查自己新加的闸能不能被绕过时发现的:把 `removeRecordV2` 里整段 claim-aware 判决 删掉,`bun test src` 一条都不红 —— 它此前是一道没人看着的闸,可以被静默删除。 而上一个提交让它的失败模式变得更坏:新增的 `refuse` 一支若无人消费,就会掉进 `delete` 支,record 与整条 claim 一起被去掉,而那个 Bundle 的图还指着它。 新增一条覆盖三种形状的闸:纯 Bundle 拥有(refuse 支,违规项不取集合首位)、 standalone + Bundle 并存(release-claim-only 支)、以及没有 Bundle 的 solo child 必须仍然去得了账(证明这不是「一律拒」)。前两支各断言字节零改动。 实测:分别删掉 `refuse` 与 `release-claim-only` 两支,这条各红一次。 Refs #706
实现方实测:这两个文件整个删掉零红,即可被静默删除。它以「登记簿的操作性判据是 断言自己模块之外的东西」为由未登记,并升给编排者裁决。 裁决:登记。判据的第一句才是本体——「删掉它就会移除某条具体保证,而不只是减少覆盖率」。 删掉 uninstall 那个文件移除的保证是「卸载属于 Bundle 的组件会拒绝而不是谎报成功」, 那是一条具体保证;而且实测删掉零红,正好落在判据的后果句上。 「断言模块之外」是发现用的启发,不是排他条件。 下界取实际条数(34 / 20),不留余量。
登记 ext-package-ledger-uninstall.test.ts 之后,登记簿的绊线立刻红了—— 它正文里明写「内层 receipt 副作用有没有被接回来不归它管,那由 ext-fs-installer.test.ts / ext-config.test.ts 里的字节零改动钉子看着」,而这两个文件既没登记也没分类。 这正是登记簿设计的作用:委派必须显式,被委派方必须在册,否则「主判据在别处」这句话 本身就是假闸门。填上 delegates_to 并把两个受托方登记(下界=实际条数 39 / 93,不留余量)。 闸门文件 65 → 69。
编排者合并前复核(本地实跑,非转述)
我自己实施的绕过(不采信实现方自报)
审计闭合Codex R1 的 MAJOR(bundle-only claim 让现役卸载谎报成功)已修: 这条 finding 推翻的是上一轮报告里一句明确的判断(「claim 必然存在,所以不可达」)—— 实现方自查绕过时又查出一个它自己开的新洞:删掉 编排者裁决:登记两个新闸门文件(实现方升上来的问题)实现方实测这两个新测试文件整个删掉零红,但以「登记簿的操作性判据是断言自己模块之外的东西」 裁决:登记。 判据的第一句才是本体 ——「删掉它就会移除某条具体保证,而不只是减少覆盖率」。 登记之后绊线立刻抓住了我:该文件正文明写「内层副作用不归它管,由 push 用 |
Fixes #706
Refs jinjunnn/alpha-work#49
把安装账本切到 V3:账本除了「装了什么」,还回答「这个 package 由哪些组件组成」(
packageGraphs)和「每个组件现在归谁所有」(
claims)。owner 是集合不是 refcount —— refcount 漏加漏减一次就永久错位且无法自证;集合每个元素都能指名道姓,refcount 由集合大小派生。
用户能看见的变化
装过 package 之后再单独卸载它的某个组件,不会再把别人还在用的东西删掉:系统先问「这个组件还有
别的主人吗」,有就一件实物都不动,只把你自己那份认领撤掉,并如实告诉你它还属于谁。
R2 审计 Blocker:收口位置
installs.json有两个物理写器。第二个(alpha-installs.writeLedger)只认receipts/records两个键,V3 新增的顶层键写一次就静默蒸发。更要命的是调用顺序 —— 卸载编排是先删实物、再去账,
而 V3 会在「仍有 Bundle owner」时拒写:判决晚一步,用户看到「卸载失败」而东西真的已经没了。
修法是删代码:
ext-fs-installer.ts×1、ext-config.ts×3),账本只由外层单点提交;planDirectUninstall)前移到删任何实物之前;refuseIfV3),挡「以后有人接回来」与「旧构建拿到 V3 账本」。把 ① 拿掉,生产代码自己会打印这句话 —— 这就是本票要消灭的状态:
「retry (idempotent)」是假的:Bundle owner 不会自己消失,重试永远失败,而实物已经没了。
本 PR 新补的三个洞(继承的实现之外)
probeLedgerForWrite不判 V3 不变量。 它是各安装路径在动任何实物之前问的那句「我等下记得下来吗」。只验信封不验不变量 ⇒ 探针放行 → 文件/配置/密钥全写完 → 落盘被拒。与上面的 Blocker
同形态,只是发生在安装侧。现在探针与落盘用同一份判据(
recordKeysOf单点)。applyPackageMutation的 exact-replay 短路跑在校验之前。 一本被篡改成 dangling claim /unknown child / 孤儿 owner 的账本,只要图恰好已是目标态,崩溃前滚就拿到
replayed: true、journal 转终态 —— 而这本账此后任何写都会被拒。校验已提到短路之前。
packageGraphOf用components[0]当 root。#749刚在隔壁把同一处缺陷删掉并留了注释(「生产者随手排的顺序,不是 root」)。这个 componentId 直接进 owner token 的派生链,认错了
= claim 归属认错人。改读声明的
role(rootComponentOf)。闸门:每一条都实施过绕过实验
planDirectUninstall前移ext-package-ledger-uninstall.test.ts5 条(skill/agent/mcp/plugin×2)package-admission挂packageMutation那行commitTransactionLedger的 carrier 分支ext-fs-installer内层removeReceipt接回来ext-fs-installer.test.ts2 条ext-config三处内层removeReceipt接回来ext-config.test.ts3 条(其中 vendored-path 那条是本 PR 新补)refuseIfV3第三行是重点:计划面的 parity test 结构上看不见「提交时账本到底写成什么」。生产 wiring 断言落在
真 IPC + 真事务之后的
installs.json上(信封 v:3、图与 envelope 绑定、graphDigest 可重算、claim owner = bundle token),那才是接线。
写次数计数器用
spyOn(fs.renameSync)数落到installs.json的原子换名。它抓不到什么已实测并写进文件头:第二次
removeRecordV2是幂等 no-op、一个字节都不写,所以它管的是「重复落盘」不是「重复调用」;内层副作用有没有被接回来由那几条字节零改动的钉子看着。如实降级,不冒充。
负向夹具一律把违规项放在非首位、集合里同时有合法项 —— 「检查每一个」写成「检查第一个」时必须红。
本地门(真实输出)
scripts/alpha-check.sh,branch 与 base(origin/alpha=e702dc13)逐项对照:node_modules软链的既有假红:两侧错误集comm -23空。bun test src连跑两次都 3486/0;delta = +47 用例、0 新红。--no-verify的理由:alpha-check.sh在origin/alpha零改动的干净 worktree 上同样exit=1(同一条 typecheck 假红)—— 已知缺陷
#754,钩子对任何 push 恒假红。北极星与测试两门均已本地跑绿并逐条对照 base。
更正:review Major「bundle-only 卸载谎报成功」(
e2cf24f7/e2056741)本 PR 此前的报告里有一句话是错的。 原文:
「claim 必然存在」为假。
package-admission的claimMutations只 acquire 一个bundle:owner;standalone owner 要等用户自己再单装一次才由
ensureStandaloneClaims写进去。所以 fresh package安装后的默认形状就是只有 Bundle owner,而
directUninstallVerdict只要看见 Bundle owner 就选release-claim-only,不问那份 standalone claim 在不在。用户看到的坏结果:对一个「只属于某个 Bundle、自己从没单装过」的条目点删除 → 界面显示「已移除」
→ 文件、record、图、claim 一件没动 → 刷新后条目依旧。修复前实测(生产
uninstallByKey,5 种 kind全中):
ok:true⇒extension-hub.tsx的onUninstall只读ok⇒ 「已移除」。而那一次落盘什么也没改变。修法(判决全部前移到写盘之前,三处各自独立成闸):
directUninstallVerdictrefuse:有 Bundle owner 但没有 standalone owner = 没有可释放的东西planDirectUninstallrefuse→ok:false(planner 据此在删任何实物之前返回失败)removeRecordV2refuse—— 若漏,去账会留下指向不存在 record 的 dangling claimreleaseStandaloneClaimwriteLedgerFile之前失败,不依赖调用方先判对「双 owner 时只释放 standalone」的行为原样保留。
连带的语义变更:
releaseStandaloneClaim对 legacy-protected-only 的 claim 从「静默成功的空操作」改为「响亮拒绝且零落盘」(生产从不走这条 —— legacy-protected 不是 Bundle,verdict 是
delete;保护本身不变)。对应测试已改写为断言拒绝 + 字节零改动。
新增的闸(修复前全红,实测):
ext-package-ledger-uninstall.test.tsbundle-only 组,5 种 kind 各一条,每条四断言:ok:false/ rename 次数 0 且字节零改动 / 零 installer 调用且实物仍在 / claim owner 集合原样;releaseStandaloneClaim直呼拒绝(违规项取集合末位),外加合法 solo child 仍释放得掉的对照;removeRecordV2最后一道闸的三形状(refuse 支 / release-claim-only 支 / solo 仍去得了账);ext-package-ledger-v3.test.ts:verdict 层 bundle-only 判决(两个 Bundle owner,违规形状不在首位)。自查绕过(改完自己先绕一遍,确认变红):
directUninstallVerdict的refuse支releaseStandaloneClaim的前置(保留 verdict 修复)removeRecordV2的refuse支第三条是自查查出来的:
removeRecordV2那道 claim-aware 闸整段删掉曾经一条不红,而refuse一支若无人消费会掉进
delete。e2056741把它补上。门(更正后的真实输出)
+8= 本次新增用例数,0新红。typecheck 仍是 202,且非packages/app的 10 条全在 renderer.tsx(同源 alias 假红),src/main零错误。scripts/assert-gate-files.shexit=0(65 个闸门文件全部在位且真的跑过)。