feat(root-abi): 接入锁定上游 compiler 并只读发布 Root ABI 制品(R-00018 / LCE-P0-005) - #8
Merged
Conversation
从只读镜像与锁定上游 compiler 产出 Header / C# Binding / Rust ContractTypes / layout report,逐份与上游 bundle 声明摘要对账后只读发布(规格 §8、§3.6)。 本 crate 是**薄适配器**,不含模板、slot 表、type map、布局常量——那些全部属于上游 compiler。在本仓自己实现模板会制造第二个 ABI 定义处,两处定义迟早分叉(规格 §4)。 tests/no_private_schema.rs 用源码级断言盯着这条边界:代码里出现 C/C# 类型拼写、 生成文本片段、写死的布局常量,或引用架构源工作区,都会红。 开工前先做了可行性实测,结论改变了实现路径:上游 packages/index.json 的 12 个语言 生成包 consumers 均不含本仓,Root ABI 三份声明输出里只有 abi/lumio_core.h 在镜像内。 按「缺上游制品」本该报 BLOCKED,但镜像里的 root-abi-bundle.json **声明了全部三份的 摘要**;以镜像输入重跑锁定 compiler 的 emitter 后,三份摘要与声明值全等。所以那两份 不在镜像里的文件不是缺口:它们由锁定 compiler 重新生成、对着上游声明摘要验证,本仓 不发明任何东西。「镜像里没有这个文件」与「本仓无法验证这个文件」是两件事。 几处实现取舍: - compiler 身份先于一切:先跑再验等于已经执行了未经核对的代码。身份摘要口径由上游 compiler_hash() 固定(sha256(lumio_contract.py || lumio_generate.py)),顺序与拼接 方式都是摘要的一部分。 - Input Hash 重算而不照抄 bundle:抄下来只能证明「我读到了这个数」,重算才能证明 「镜像里的输入确实就是产生这个数的那份」。 - descriptor 的构造抽成 generate 与 verify 共用的**唯一**一处。首版让 verify 跳过 descriptor 自身、注释写「完整性由它记的每一条都对得上间接证明」——那句是错的, 往末尾加一个空格完全不被发现。hand_editing 测试当场抓到,改为按同一规则重建后 逐字节比对。 - --plan 不再被静默忽略:计划经 composition::verify_frozen_plan 读取(ADR 0006 第 8 条: 消费者不得自建第二套解析器),其 architecture 基线与提交必须与本仓 lock 一致, 否则「按 A 计划构建、按 B 基线生成 ABI」会一路无声走到运行时。 justfile 的 check-generated 并入本卡的 verify —— 该 recipe 的原注释就写着「root-abi generator 的 verify-generated(LCE-P0-005)落地后按规格再并入本 recipe」。不并入的话 生成物没有任何门禁守着,手改不会被发现。对照组实测:改一个字节 → exit 3 → 重建 → 绿。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
reviewer 四条 P1,共同的形态是「摘要链的上半段是真的,下半段是自证的」。他实测出两条 绕过路径都能让 just check-generated 绿灯,其中一条能把注入内容送进 C Header 与两份 binding。 P1-1 metadata/ 与 reports/ 两份产物没有任何独立锚点。它们唯一的约束是 descriptor 的 fileDigests,而 descriptor 正是从同一批盘上字节重建出来的——被背书者与背书者同源, 是恒等式不是校验。整份替换 ABI 文档再重建 descriptor,verify exit 0。 补锚点:ABI 文档锚回镜像里 inputSet 声明的那一份(逐字节相等,已被 inputHash 钉死); layout report 锚回本次由上游 layoutProfile 现算的内容(那个值第 425 行本来就算出来了, 之前直接丢弃)。 P1-2 摘要链的根(上游 bundle)未与 lock 对账。read_bundle 无条件采信它声明的 compiler.digest / inputHash / outputFiles[].digest,而 lock 的 requiredPathSha256 里 正有这份文件的登记摘要——没读。改 bundle 里三个 outputFiles.digest 再重建 descriptor, 六份产物可被整体替换。改为读 lock 校验后才解析。 P1-3 require_compiler! 用 eprintln! + return 实现「跳过」——那等于测试通过,libtest 还 会把提示吞掉。在没取过工具链的机器上,6 个测试恒为绿且恒为空,而本卡四条通过条件的 全部证据都挂在它们身上。宏自己的注释还写着「静默跳过与通过长得一样,那正是 B-00002 要消灭的形态」,随后做的就是这件事。改为缺工具链即失败。 P1-4 卡面要求的上游 semantic validator 从未被调用,报告却无条件声称已通过。上游 emit_root_abi 的第一件事是 validate_abi_document(schema + ADR-040 语义),docstring 明写 「在写出任何一个输出字节之前拒绝非法 ABI 文档」;驱动脚本重写它的主体时把这次调用 跳掉了,同时 AbiCompatibilityReport 的 schema_valid / semantic_rules_valid / symbols_valid 写死 true——9 个字段里 7 个是常量或恒真式,compiler_lock.rs 那条 assert!(report.schema_valid) 是在断言一个字面量。 改为:驱动脚本先跑 validate_abi_document;schema/semantic 依据 descriptor 记录的 validatorRan(descriptor 已被逐字节重建校验,改不动);symbols 真查一次——ABI 文档声明的 entrySymbol 必须出现在发布的 C Header 里。 跑上游 validator 需要 lumio_contract 能按仓库根布局读 fixture(它在导入时就读), 所以用只读镜像的 schemas/ fixtures/ ids/ packages/ 加锁定 tools/ 在临时目录拼一个 一次性 contract root,用完即删;镜像本身不动,绝不使用架构源仓工作区。 新增负向测试覆盖 reviewer 实测的两条绕过、bundle 篡改、compiler 字节漂移分支。 13 个测试全绿(原 10 + 3)。 同批处理的 P2: - no_private_schema.rs 的能力边界写进文档(前三条是关键词黑名单,reviewer 给出五种 现成绕法),并补一条真正结构性的断言:src/ 下不得有非 .rs 文件——挡住「模板外置成 templates/*.h.in 再 include_str!」这条最省事的路。 - descriptor 补输入逐文件摘要(§3.6):只有聚合 inputHash 时,镜像漂移只能说「不一致」, 指不到是哪个文件。 - CLI 子命令改成规格 §8.4 的 verify-generated --generated。 - frozen_plan_path 的 Option 意味着核对可跳过,这条弱化写进注释与 ADR,不再只说「为什么 是路径」。 沉淀 ADR 0009:摘要链每一环都要有外部锚点、descriptor 不得自证、报告不得声称没做过的 检查、上游输出集合以上游为准。判据写成可复用的一句:被背书者与背书者必须不同源。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
修 P1-4 时引入了 ADR 0009 第 1 节判据的直接违例:重建 descriptor 时,validatorRan 与 entrySymbol 是从**被校验对象自己**取回去的,于是逐字节比对对这两个字段恒真。reviewer 实测:只改 descriptor 一个字段、不动任何产物,verify-generated 直接 exit 0。 这条盲区正好落在 hand_editing 测试的形状之外——那个测试往每份文件**追加**一个空格, 追加会被重建比对抓到,**替换**自证字段的值不会。 两处分别修,理由不同: - entrySymbol 有外部真值:镜像里的 ABI 文档(已被 inputHash → bundle → lock 钉死)。 重建与 symbols 检查都改用它,descriptor 里那份只作记录不作判据。 - validatorRan 记录的是生成期事件,回读期**不可能**外部重建。所以不假装它受保护: 重建时传字面量 true,任何非 true 的取值都造成字节不符而硬失败,语义上等于 「自称 validator 没跑过的制品一律拒收」。这同时补上了「validatorRan=false 仍 exit 0」 这个门禁洞。 原则写进 ADR 0009 第 3 节:**回读期无法外部重建的字段,不要假装它受保护**——要么以 字面量参与重建,要么换一个有外部真值的来源。同时订正三处做了错误安全声称的文档 (lib.rs、compiler.rs、ADR 第 3 节与后果段)——ADR 第 1 节的判据与第 3 节的机制此前是 互相矛盾的,那是本卡新写的规范,不能带着自相矛盾进主干。 新增 replacing_a_self_reported_descriptor_field_is_caught 覆盖三组替换。 同批 P2: - 删掉未经 lock 校验的 read_bundle(它紧挨着 read_bundle_verified,下一个人有一半概率 拿错那个)。 - ABI 文档路径统一由 input_set::abi_document_path 给出,并断言它**在 inputSet 内**—— 「在 inputSet 内」正是这个锚点成立的前提,此前只写在注释里;同时与 compiler 自报的 路径交叉核对,上游改 ABI_DOCUMENT 时响亮失败。 - 临时 contract root 改排他 create_dir,与 publish.rs 同一口径。 - error.rs 补明 CompilerInvocationFailed 这一桶同时覆盖宿主环境缺失与 ABI 内容非法两类, 不要按码值反推语义(reviewer 复审后不坚持拆码,理由是加进 validator 后这一桶里确实 装着真实的内容失败)。 测试 13 → 15(compiler_lock 10 + no_private_schema 5);上轮交回物写的 13 是错的。 三摘要不变:217437fd… / 696a58d0… / bdbab5d3…。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
reviewer 复审放行时点名这条够格进 lessons:同一失效模式在本批出现两次同型 (ADR 0007 明写用 toml crate 却手写扫描;ADR 0009 写下「被背书者与背书者必须不同源」 却在同提交里让 descriptor 两个字段自证),相邻同族形态还有三次。 根因不是粗心:写规范与执行规范之间没有机械检查,而刚写完规范时最容易觉得自己已经 遵守了。五次里没有一次是通读代码发现的,全部来自实跑构造的反例——已有的 grep 自验 规则只能证明「被声称的 X 存在」,证明不了「X 覆盖的范围等于声称的范围」。 规避写成可验证的行为:判据与它的反例测试同一提交内同时出现;反例的构造方式要覆盖 多种形态(追加一个字节与替换某字段的值覆盖面完全不同);声称「由 X 覆盖」时另问一句 「X 挡不住的是什么」并写进文档。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
做了什么
从只读镜像与锁定上游 compiler 产出 Header / C# Binding / Rust ContractTypes / layout report,逐份与上游 bundle 声明摘要对账后只读发布(规格 §8、§3.6)。
本 crate 是薄适配器:不含模板、slot 表、type map、布局常量——那些全部属于上游 compiler。在本仓自己实现模板会制造第二个 ABI 定义处,两处定义迟早分叉(规格 §4「私有模板会制造第二 ABI」)。
tests/no_private_schema.rs用源码级断言盯着这条边界。开工前的可行性实测改变了实现路径
上游
packages/index.json的 12 个语言生成包consumers均不含本仓;Root ABI 三份声明输出里,只有abi/lumio_core.h在镜像内——另两份(Rust / C# binding)所属 package 的 consumers 不含本仓,被 R-00263 的投影规则正确排除。按「缺上游制品 → BLOCKED」我本该在这里停。但镜像里的
root-abi-bundle.json声明了全部三份的摘要。实测:217437fd…f290696a58d0…5bb3那两份不在镜像里的文件不是缺口:它们由锁定 compiler 重新生成、对着上游声明的摘要验证,本仓不发明任何东西。
「镜像里没有这个文件」和「本仓无法验证这个文件」是两件事。 前者成立、后者不成立,我差一步就把前者当后者报了 BLOCKED。
几处实现取舍
compiler 身份先于一切。 先跑再验等于已经执行了未经核对的代码。身份摘要口径由上游
compiler_hash()固定,顺序与拼接方式都是摘要的一部分。Input Hash 重算而不照抄 bundle。 抄下来只能证明「我读到了这个数」,重算才能证明「镜像里的输入确实就是产生这个数的那份」。
descriptor 的构造抽成 generate 与 verify 共用的唯一一处。 首版让 verify 跳过 descriptor 自身,注释写「完整性由它记的每一条都对得上间接证明」——那句是错的,往末尾加一个空格完全不被发现。
hand_editing_any_generated_byte_makes_verification_fail当场抓到,改为按同一规则重建后逐字节比对。--plan不再被静默忽略。 计划经composition::verify_frozen_plan读取(ADR 0006 第 8 条:消费者不得自建第二套解析器),其 architecture 基线与提交必须与本仓 lock 一致——否则「按 A 计划构建、按 B 基线生成 ABI」会一路无声走到运行时。验证证据
just generate-abi p0-linux打印三者;verify回读复算全部匹配OutputHashMismatch/UnregisteredFile拦下(含 descriptor 自身)cargo test -p lumio-core-root-abi-generator10/10;just checkexit 0;just nextest75/75;just check-contractsexit 0;spec-lint OK。一处越界,理由在这里
justfile的check-generated并入了本卡的 verify。该 recipe 的原注释写着:「root-abi generator 的 verify-generated(LCE-P0-005)落地后按规格再并入本 recipe」——这是 LCE-P0-001 预留给本卡的接入点。不并入的话,生成物没有任何门禁守着,手改不会被任何检查发现(这正是上一张卡 P1-1 的形态)。对照组实测:改一个字节 →exit 3→ 重建 → 绿。已知缺口
python3,而tools/tools.lock.toml没有它(该文件只有 R-00265 能写)。已上报总调度;倾向按「同 git / node 的宿主基础设施」处理——两者也都在用且都没登记,单独给 python3 立规矩反而不一致。compiler_path+compiler_digest合并为compiler_directory(compiler 身份由两个文件共同决定,单路径表达不了;期望摘要必须来自上游 bundle 而非调用方传入,否则调用方能绕过对账);build_plan: FrozenBuildPlan改为frozen_plan_path: Option<PathBuf>(Root ABI 输入集合全部来自上游inputSet,计划的作用是交叉核对基线/提交)。两处都写进了类型文档。just fetch-architecture-tools需要LUMIO_ARCHITECTURE_REPO:sync 脚本按..//../../找同级源仓,worktree 在<repo>/.claude/worktrees/<name>下找不到。属既有脚本行为,未在本卡修改。🤖 Generated with Claude Code