[ADR-040][CODE] 合同层回滚:摘掉 opencode-plugin profile 与 engine:config/engine:plugin,宿主 profile 集合回到四个 - #833
Closed
jinjunnn wants to merge 2 commits into
Closed
[ADR-040][CODE] 合同层回滚:摘掉 opencode-plugin profile 与 engine:config/engine:plugin,宿主 profile 集合回到四个#833jinjunnn wants to merge 2 commits into
jinjunnn wants to merge 2 commits into
Conversation
…ne:plugin,宿主 profile 集合回到四个 ADR-040「被否决的方案 C」的合同半场。行为层已由 `#825` 封死(用户装不了插件), 本票把合同也摘干净:profile 五 → 四,capability 五 → 三,`maxScriptAssetBytes` 连同 `opencode-plugin` 的解码臂、payload schema、语料向量一起撤回。 **照内容剔除,不是 `git revert`** —— `6126e69e` 里混着与插件无关的真实修复, 逐条核过并保住:kind 分流单点表与 `packageChildPreviewKeyV1`(只撤表里那一行, 表本身与判据不动)、`aw#112` 的资产字节前移与 `package-mixed-bundle.fixture` 抬头那段实况改写、`docs/contracts/host-extension-package-v1.md` 的「这是转述散文 不是被校验的 pin」与 producer pin 前移、`#807` R1/F1 那条「optional 叶子上的畸形 capability 必须整包被拒」的回归闸(文法收回原样后 `a::b` 仍然违规,判据不变)。 `#809` M1 的「卸载目录删不掉不再谎报成功」按 ADR 实读修正**不抢救**:回滚前 `ext-package-uninstall.ts` 里 `rmSync` 与 `catch` 各零命中。 capability 文法两处同步收回 `^[a-z][a-z0-9.-]{0,95}$`。`#807` 当初加冒号时是人拿 程序比对了一次,仓里没留下任何东西看着它 —— 本票把那次一次性比对变成常驻闸: decoder 导出 `PACKAGE_CAPABILITY_GRAMMAR_V1`,artifact 测试断言它与信封 schema 的 `$defs/capabilities.items.pattern` **`.source` 逐字相等**,并拿一组**写死字面量**的 探针(含 `engine:config` / `a::b` / `a:` 三个 `#807` 原始反例)同时跑双方 —— 只比 `.source` 的话,把两边一起改错仍然自洽。 `host-extension-package-artifact.test.ts` 那条曾被翻正的断言翻回反向 (`not.toContain("opencode-plugin")`);Phase 4 加的完整绑定 exact-set (id@version + mediaType + schemaPath)**保留**并收到四条 —— 它挡「悄悄加一个」, 反向断言挡「这个名字回来」,两条方向不同,删任一条都留口子。 **测试条数变化(逐条点名)**:ui-mac 3739 → 3731(-8),文件数 264 不变。 - `package-envelope-v1.test.ts` 86 → 80:script 资产的两条界/mediaType 负例、 plugin 解码臂自己的三条 strict 轴负例(未知键 / 缺 asset / bytes 是字符串)、 以及「把界挪到 4096 看边界跟不跟着走」那条上界闸 —— 六条全部只测 `opencode-plugin-v1` 这一档语料,该语料随 profile 一起撤,判据无处可落。 - `package-secret-prerequisite.test.ts` 20 → 18:`#809` 的两条(plugin 密钥前置 显式空集 / plugin payload 塞 requiredSecrets 解码期被拒)。前者钉的是「对 plugin 这个 profile 空集是正确答案」,profile 没了这句话没有主语;后者的咽喉是 plugin payload schema 的 `additionalProperties:false`,那份 schema 已删。 - `package-alpha-connection.test.ts` 55 → 54:`#809` connection 半场那一条,同因。 - `host-extension-package-artifact.test.ts` 6 → 7:**新增**上面那条文法逐字一致闸。 - `package-plugin.wiring.cases.ts` 3 → 4(子进程,条数钉在 wrapper 里):`#825` 的 拒绝用例从一条拆成两档 —— 合同层撤回后拒绝点从 admission 咽喉提前到解码期, IPC 面拿到的是 reason code 而非那条具名消息,于是「具名」改由合同回答(同一份 信封字节喂生产 decoder,断言它点得出是**那个** plugin 组件、且四个正常组件一个 都不出现在错误里),两档输入分别只踩 capability 词表与 profile 注册表,任一半被 悄悄加回来都红。 `scripts/gate-files.tsv` 两个下界随之下调(alpha-connection 55 → 54、 secret-prerequisite 20 → 18),wiring 那行的保证文字改写为实况。 **跨仓 pin 三跳未做**,由主 session 协调:新的 host `artifactSha256` = `11d6c02624abcbeb121f3c1c6ffc08314398948e6d006514416aef14fd62ca73`。 在 alpha-web 重发 + 回来 re-vendor 之前, `alpha-contracts-consumer/src/extension-package-artifact.test.ts` 的 「vendored 宿主副本与本仓活 artifact 逐字节相同」**预期红**(53 pass / 1 fail); 本票不动那条 pin。 Fixes #830 Refs #825
…profile / 三 capability,managed-plugin 放回排除表 跨仓 pin 三跳的最后一跳。alpha-web#138 已合并(`main` = `a8e6d52`),它钉的 host pin 正是本分支上一个 commit:`commit = ead7a84…` / `sha = 11d6c026…`。本 commit 把 producer 产物落到消费侧,于是 `#830` 上一跳的**预期红转绿**。 **语料 39 → 36。这是本清单第一次有文件被撤掉** —— `input.plugin.valid.json`、 `expected.plugin.compiled.json`、`asset.generic-plugin.js` 三份随 `opencode-plugin` 一起走。vendor 循环「只写不删」,所以这三份 vendored 字节是**手动 `git rm`** 的; 抓漏删的是 `extension-package-artifact.test.ts` 的目录清单断言(vendor 目录实际条目 必须恰等于 lock 的 files 集),那条判据在这一跳第一次真正做功(`#811` 那跳零删除)。 **「36 vs 35」不是噪音,是那条 `producerCommit.embedded: false` 的另一半**: lock 数 = 目录里全部文件(36);producer manifest 数永远少一个(35)—— manifest 装不下自己的哈希。这个差不写成第二个常数,由 `lock.files == manifest.files ∪ {manifest}` 那条 `toEqual` 对死。 `check:vendor` 的输出独立复述同一件事: `checked 36 contract artifacts … 35 producer-manifest hashes ↔ source-pinned aggregate 5546c08d… OK`。 逐文件复核未走 vendor 脚本自己的循环(那是自指):独立脚本把 36 份 vendored 字节与 `git show a8e6d52:contracts/extension-package/artifact/<name>` 逐个算 sha256 —— **集合相等、36/36 零不匹配**。 `extension-package-artifact.test.ts` 的 closure exact-set 跟着回到真值: - `registry.profiles` / `generic-profiles.mappings` 五 → 四; - `registry.capabilities` 五 → 三(`rules.capabilities` 与它互为判据,未单独写死); - `rules.excluded` **放回 `managed-plugin`**,回到六项; - `vectors.valid` 五 → 四(`input.plugin.valid.json` 已不存在); - lock 条数常数 39 → 36,pin 的 commit / aggregate 同步前移。 **测试条数零变化**:consumer 仍是 54 pass / 5 files(与 base 相同), `extension-package-artifact.test.ts` 仍 7 条 —— 本 commit **没有删掉任何测试**, 只改期望值。票面没点名的第四个连锁:`scripts/vendor-lock-degradation.test.ts` 把旧 pin 写进了降级输出的断言字符串(`no staged checkout of …@9fcd83d…` 与 `not.toContain("verified 39 contract artifacts from")`),两处随 pin 前移。 Refs #830
jinjunnn
pushed a commit
that referenced
this pull request
Aug 3, 2026
…并 re-vendor 到 aw@a8e6d52 (#833) 宿主 profile 集合回到四个(agent / mcp-local / mcp-remote / skill), capability 回到三个;`managed-plugin` 放回发布端 excluded。 跨仓 pin 窗口在本跳闭合:vendored 副本与本仓活的 host artifact 逐字节一致。 依据 ADR-040「被否决的方案 C」。Fixes #830. Refs #825. 本次为本地 squash-merge:GitHub GraphQL 端点持续 EOF,按 alpha-work/CLAUDE.md 的兜底路径改用本地合并 + git push(git 走 github.com 可靠)。 本地 alpha-check.sh 7/7 全绿已复验。
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.
Fixes #830
Refs #825
ADR-040「被否决的方案 C」的合同半场。行为层已由
#825封死(用户装不了插件),本票把合同也摘干净:profile 五 → 四、capability 五 → 三、
maxScriptAssetBytes连同
opencode-plugin的解码臂、payload schema、语料向量一起撤回。照内容剔除,不是
git revert——6126e69e里混着与插件无关的真实修复。五条「必须保住」逐条确认
^[a-z][a-z0-9.-]{0,95}$,并新加一道常驻闸(此前仓里没有任何东西看着它)host-extension-package-artifact.test.ts新增the capability grammar is byte-identical …;三次绕过实验见下matrix.tsv的测试标题锚点req128-capability-matrix.test.ts第三条会把 matrix 里的 evidence 名字与真实test(...)标题对死 ⇒ 改名即红。故意保留schemas are strict, payload-ref-only, and bind every registered profile exactly这个标题(回滚后它仍然为真,只是绑定的 profile 从五个变四个)git diff --stat里无matrix.tsv;该闸 3 passaw#112的资产字节前移package-mixed-bundle.fixture.ts抬头那段实况改写原样保留git diff --stat里无package-mixed-bundle.fixture.ts、无vendor/buildPayload显式拒绝E_HOST_PARSER_DRIFT修复不需要抢救的那条:
#809M1「卸载目录删不掉不再谎报成功」按 ADR 实读修正未抢救 ——回滚前
ext-package-uninstall.ts里rmSync与catch各零命中(已复核),没有可留的来源。另外三条与插件无关、随手会被一起删掉但保住了的:
docs/contracts/host-extension-package-v1.md的「这是转述散文不是被校验的 pin」两段与 producer pin 前移;#807R1/F1 那条「optional 叶子上的畸形 capability 必须整包被拒」的 graph 回归闸(文法收回原样后a::b仍然违规,判据不变);kind 分流单点表与
packageChildPreviewKeyV1(只撤表里"opencode-plugin": "plugin"那一行,表本身与 A/B 两条判据不动)。新增的那道闸,和它为什么不是「多写的抽象」
票面要求「两处必须仍然逐字一致,并且要有断言钉住」。实读:仓里没有这样的断言 ——
#807当初两边同步加冒号时是人拿程序比对了一次。所以 decoder 导出PACKAGE_CAPABILITY_GRAMMAR_V1,测试断言它与信封 schema 的$defs/capabilities.items.pattern.source逐字相等,再拿一组写死字面量的探针同时跑双方。只比
.source不够 —— 那是本仓记录在案的「比较基准与被测对象同源」形态:把两边一起改错仍然自洽。所以探针的期望值不从任何一边派生,含
engine:config/engine:plugin/a::b/a:/a:b:c:d五个必须为假。绕过实验(全部实跑输出)
AC ① — 三处 exact-set × 两个方向(直接驱动生产函数
assertHostExtensionPackageRegistryV1()):AC ② — 反向断言。绕过配方取最不利的一种:一个「认真的」重新引入者把 profile 加回
registry JSON,并把三处正向清单全部跟着改(
registry.ts的 exact-set、测试里的 profileId 清单、完整绑定清单)。此时唯一还站着的就是那条反向断言:
⇒ 我保留了 Phase 4 加的完整绑定 exact-set(收到四条)而不是一并删掉:
它挡「悄悄加一个 / 指错 schema」,反向断言挡「这个名字回来」,方向不同,删任一条都留口子。
新文法闸的三次绕过(含最难的一次:两边一起改成自洽):
三次实验前后
git status均干净、HEAD未动,还原后复跑host-extension-package-contract三个文件 89 pass / 0 fail。AC ③ —
bash scripts/alpha-check.sh各步真实结果(re-vendor 之后:7/7 全绿)跨仓窗口的对面(
alpha-web#138/ PR #140)已合并,本 PR 第二个 commit 完成了 re-vendor,上一版正文里那条「预期红」已经转绿,
alpha-check.sh现在EXIT=0:那条曾经红的判据,按名字单独复跑:
步骤 [4] 这次没有短路(它是
&&链;上一版 consumer 一红就把 ext 与 ui-mac 一起吞掉了),三个包都在同一步里真的跑过:
与 base(
alpha@1eba7f8d)的 fail-set 差:零。base = ui-mac 3739/0/264、ext 133/0/10、consumer 54/0/5、alpha-check 7/7;
现在 = ui-mac 3731/0/264(−8,逐条点名见下)、ext 133/0/10(相同)、
consumer 54/0/5(相同)、alpha-check 7/7。没有任何新增的红。
第二跳:re-vendor 到
alpha-web@a8e6d52maina8e6d52e05430623ec7b91ae078d902f0e11a614artifactSha2565546c08d541851f860d0e6835bf58db41acdc6447e1393f81e5bb8ed6c276261commit = ead7a84f…(= 本 PR 第一个 commit)、sha = 11d6c026…(= 本 PR 产出的 artifact)逐文件 sha256 比对:36/36 零不匹配,文件集合完全相等。 复核没有走 vendor 脚本自己的
循环(那是自指等价链),而是独立脚本把每份 vendored 字节与
git show a8e6d52:contracts/extension-package/artifact/<name>各自算一遍:「36 vs 35」的差能解释,不是噪音:lock 数 = 目录里的全部文件(36);
producer manifest 数永远比它少一个(35),因为 manifest 装不下自己的哈希
(
producerCommit.embedded: false是同一个理由的另一半)。这个差不写成第二个常数,由
lock.files == manifest.files ∪ {manifest}那条toEqual对死。check:vendor的输出独立复述同一件事:checked 36 contract artifacts … 35 producer-manifest hashes ↔ source-pinned aggregate 5546c08d… OK。这一跳是该清单第一次真的删文件。 vendor 循环「只写不删」,所以
input.plugin.valid.json/expected.plugin.compiled.json/asset.generic-plugin.js三份是手动
git rm的 —— 抓漏删的是目录清单断言(vendor 目录实际条目必须恰等于 lock 的files 集);它在
#811那跳零删除,这一跳才第一次真正做功。closure exact-set 跟着回到真值:
registry.profiles/generic-profiles.mappings五 → 四;registry.capabilities五 → 三(rules.capabilities与它互为判据,没有单独写死);rules.excluded放回managed-plugin,回到六项;vectors.valid五 → 四;lock 条数常数 39 → 36,pin 的 commit / aggregate 同步前移。
closure exact-set 两个方向各绕过一次(三组,全部实跑)
六次实验前后
git status干净、HEAD未动,还原后 consumer 复跑 54 pass / 0 fail。AC ④ — 测试条数下降,逐条点名
ui-mac 3739 → 3731(−8),文件数 264 不变。加减逐项对得上:
package-envelope-v1.test.tsscript asset one byte over the registered maxScriptAssetBytes②script asset refuses a markdown media type③plugin payload refuses an unknown behavior key④plugin payload refuses a missing asset⑤plugin payload refuses a string asset byte count⑥the script asset limit accepts exactly the registered value and refuses one byte more。六条全部只作用在opencode-plugin-v1那一档语料上,该语料随 profile 一起撤 ⇒ 判据无处可落package-secret-prerequisite.test.ts#809 managed plugin 的密钥前置显式为空集(具名,不是走兜底 else)—— 它钉的是「对 plugin 这个 profile,空集是正确答案」,profile 没了这句话没有主语;②#809 plugin payload 里塞 requiredSecrets ⇒ 解码期就被拒(schema 不许额外键)—— 它的咽喉是 plugin payload schema 的additionalProperties:false,那份 schema 已删package-alpha-connection.test.ts#809 managed plugin 的 Alpha Connection 前置 > 显式为空集(具名,不是走「非 mcp-remote 就返回空」那条兜底),与上面①同因host-extension-package-artifact.test.tspackage-plugin.wiring.cases.ts(子进程)scripts/gate-files.tsv两个下界随之下调:package-alpha-connection55 → 54、package-secret-prerequisite20 → 18(本仓约定「下界=实际条数,不留余量」)。package-envelope-v1的下界 74 本来就低于 80,未动。票面没说到的连锁(四条)
1.
#825的行为闸会红,因为它的输入不再表达得出来。 实测:回滚后那个包在 IPC 面拿到的reason变成"package-invalid",原断言expect(reason).toContain(LEAF_PLUGIN_ID)/toContain("ADR-040")当场红 —— 拒绝点从 admission 咽喉提前到了解码期。处置:拒绝用例拆成两档输入,分别只踩合同层的一半 —— 一档声明
engine:config/engine:plugin(踩 capability 词表,header 阶段停,
package-invalid),一档不声明任何 capability(只踩 profile 注册表,support 阶段停,
package-host-update-required)。「具名」改由合同自己回答:同一份信封字节喂生产 decoder,断言它点得出是那个 plugin 组件的
哪个字段,且四个正常组件一个都不出现在错误里。IPC 面继续钉位置承诺
(不到授权屏 / 第三方 JS 零字节 /
runExtensionTransaction从未调用 / 盘面四个面零变更)。若只留一档,把 profile 悄悄加回注册表时另一档仍会被文法拦下而不红。
2. kind 分流表的键集断言强制它跟着改。
package-child-kind.test.ts断言表的键集==PROFILE_REGISTRY_V1的 profileId 集,所以"opencode-plugin": "plugin"必须撤。顺手把
opencode-plugin放进「未登记 profile 具名拒绝」那条的枚举首位 —— 加回表里即红。pluginkind 本身保留:seed / legacy 通道用同一套事务 key 与授权账位置,那条通道不经过本表。3.
main/package-admission.ts里kind === "plugin"那道咽喉现在在 package 通道上到不了,我没有删它。理由:它是
PackageChildKindV1上的穷举兜底 —— 哪天有第二个 profile 被映到plugin,它在写盘之前挡住,而不是靠「注册表里没有它」这条外部前提。删掉 = 把 fail-closed 换成默认放行,
而且那是行为层、属
#825的边界。只把那段已经变成谎话的注释改成实况。4. 一个语义偏差,登记不修: 未登记 profile 落在 support 阶段 ⇒ 用户看到的是
update-required/package-host-update-required(「升级 Alpha 就能装」)。对opencode-plugin这是假的 —— ADR-040 裁定没有哪个未来版本会装它。这是宿主对任何未知 profile 的通用机制,
不在本票边界内,也不影响「装不上」这件事。
跨仓 pin 三跳:已闭合
host⚠️ vendored producer closure
artifactSha256=11d6c02624abcbeb121f3c1c6ffc08314398948e6d006514416aef14fd62ca73(第一个 commit 产出)→
alpha-web#138消费并反向钉住它 → 第二个 commit re-vendor 回来。三跳闭环;
docs/contracts/host-extension-package-v1.md里那段「比宿主 registry 落后一跳」的临时登记已随之删除。
第二个 commit 的测试条数:零变化。 consumer 仍 54 pass / 5 files,
extension-package-artifact.test.ts仍 7 条 —— 没有删掉任何测试,只改期望值。票面没点名的第四个连锁:
scripts/vendor-lock-degradation.test.ts把旧 pin 写进了降级输出的断言字符串(
no staged checkout of …@9fcd83d…与not.toContain("verified 39 contract artifacts from")),两处随 pin 前移。
顺带发现,已记下不顺手改
ac#815这次抓到了确切的触发条件,值得写进那张票。从 pre-push 钩子跑闸门时(不是直接跑
bash scripts/alpha-check.sh),会往当前分支写入若干
root commit /private/var/.../opencode-test-*的空提交(作者Test <test@opencode.test>),并把共享.git/config改成core.bare = true、写回
user.name/email = Test。alpha-check.sh两次,HEAD与 config 都没动;经钩子推那一次,
HEAD从80828b61漂到764e70e3(三个空提交)且 config 被改。scripts/git-cross-repo.ts抬头注释逐字吻合:「
git pushexports GIT_DIR into the hook process and GIT_DIR beats-C」。那次修复覆盖了 vendor 脚本的跨仓 git 调用,但建临时 git 仓的测试夹具没有走它,
于是它们的
git commit/git config落到了这个仓上。core.bare = true写在一个有工作树的共享仓上,会让主 checkout 的一部分 git 操作直接报 "this operation must be run in a work tree"。
git reset --hard掉那三个空提交(它们 tree 全空,git diff --stat 80828b61 HEAD为空,内容零损伤),改用
--no-verify推,并把共享 config 修回core.bare=false/ 清掉user.*(主 checkout 已复验:
--is-inside-work-tree为 true、在alpha上、工作树干净)。我没有去改那些测试夹具 —— 那是
ac#815的边界,不是本票的。push 用了
--no-verify:不是因为闸门红 —— 闸门是 7/7 绿,而且我先带钩子推过一次,钩子自己也打印了
✅ all local gates green。用--no-verify是因为上面那条ac#815:带钩子推会往分支写空提交并改坏共享
.git/config。两次 7/7 全绿的输出都贴在 AC ③。