Skip to content

[REQ-128][CODE] 宿主合同注册 opencode-plugin profile,并把 engine:config/engine:plugin 登记进 host capability registry - #814

Merged
jinjunnn merged 3 commits into
alphafrom
feat/807-opencode-plugin-profile
Aug 3, 2026
Merged

[REQ-128][CODE] 宿主合同注册 opencode-plugin profile,并把 engine:config/engine:plugin 登记进 host capability registry#814
jinjunnn merged 3 commits into
alphafrom
feat/807-opencode-plugin-profile

Conversation

@jinjunnn

@jinjunnn jinjunnn commented Aug 3, 2026

Copy link
Copy Markdown
Owner

大白话

宿主合同今天认四种载荷。这张票加第五种:managed OpenCode Plugin —— 一段会在引擎进程内、以引擎自己的权限执行的 JS。

前三相 opencode-plugin 不是被忘了,是被主动挡着:三处 exact-set 加一条反向断言(host-extension-package-artifact.test.ts:165 明写 registry 不许出现这个词)。本票把那条闸翻成正向,而不是删掉它——只删等于少一道闸。

能力不新造词。 用既有的 engine:config + engine:plugin(renderer 授权屏对 legacy 插件安装一直用的那一对,shared/ext-capability-authorization.ts:30 今天就同时申请这两个)。新造一个只披露"会执行 JS"的词,会让 managed 路径比 legacy 路径披露得更少 —— 用户看不到"这东西会写你的引擎配置"。

退出条件逐条兑现

# 退出条件 结果
bun generate-artifact.ts --check exit 0 checked 13 HostExtensionPackageV1 files / exit=0
bash scripts/alpha-check.sh 全绿 7 步里 5 步绿;2 步红,红的根因只有一个,是 T4 的预期红(见下)
artifactSha256 f1fd0476477cf02b666615adce0d6abcd985f6b77b3d0df73ca2800bd5b62bcf
derive 矩阵覆盖两个新 capability 覆盖。矩阵的判据是 expect(reachable).toEqual(vocabulary),vocabulary 从 registry 读 ⇒ plugin 少派生任何一个都红(实测见绕过 ④)
maxScriptAssetBytes 恰好值接受 / +1 拒绝,期望值从 registry 读 两向都有用例,期望值一律 HOST_EXTENSION_PACKAGE_LIMITS_V1.maxScriptAssetBytes,零字面量
maxCapabilities 实读 + 复核结论 见下

maxCapabilities 复核(自己实读,不是转述)

maxCapabilities = 16。它是 decoder.ts:685decodeCapabilities()数组长度上限,而 decodeCapabilities 只有两个调用点::372(envelope.capabilities)与 :639(单个 component 的 capabilities)。它不是 registry 词表大小的上限,词表从 3 涨到 5 与它无关。

实测:语料里单组件 capabilities 最多 2 条(opencode-plugin 恰好声明 ["engine:config","engine:plugin"]),单信封并集最多 2 条;即使一个包把五个 token 全用上,5 <= 16 仍成立。⇒ 本票不动这条界。

边界

只碰 packages/ui-mac/src/shared/host-extension-package-contract/(加一份 docs/verification 证据台账的同步,见下)。宿主实现(admission / 事务 / 卸载)、renderer、alpha-web 一行未动 —— 那些是 T2a / T2b / T5 / T3 的边界。

⚠️ 基线连锁表(§4 D2)没点名、实跑才暴露的两处

1. capability 文法根本不允许冒号 —— 不改的话这个 profile 结构上发不出来。

decoder.tsCAPABILITY_REalpha-package-envelope-v1.schema.json$defs/capabilities.items.pattern 都是 ^[a-z][a-z0-9.-]{0,95}$。实跑:

alpha.connection.v1 true
engine:config false
engine:plugin false

engine:config 连进不了信封,decodeCapabilities 会先报 invalid format,根本走不到 registry membership 那一步。两处同步加 :

这个放宽不放宽准入面:该正则只是文法/DoS 界,真正决定一个 token 认不认的是 isPackageCapabilityV1 的 registry membership(registry.ts,membership 而非前缀)。两处必须逐字一致 —— 一个过了发布 schema 却被宿主 decoder 拒掉的 producer,意味着合同在说谎。

2. req128-capability-matrix 按测试标题钉死证据。

docs/verification/2026-07-31-req128-capability-matrix/matrix.tsv:14schemas are strict, payload-ref-only, and exclude Phase 2/4 profiles 这个标题字符串登记为证据。那个标题在本票之后是假话(它不再排除 Phase 4),改名后 matrix 自闸当场红。同步改了那一格。这是那道闸在正常工作,不是它挡路。

绕过实施记录(四条,正反两向,原始输出)

基线 bun test src/shared/host-extension-package-contract = 90 pass / 0 fail / Ran 90 tests across 3 files

① 往 registry 加第六个 profile 而不改 exact-set ⇒ 必须红

MUTATION APPLIED: sixth profile 'rogue' added, exact-sets untouched
94 |     throw new Error("profile registry must contain the sorted host profile set exactly once")
error: profile registry must contain the sorted host profile set exactly once
(fail) HostExtensionPackageV1 artifact > publishes the fixed path, sorted registries, and exact per-file SHA-256
(fail) HostExtensionPackageV1 artifact > schemas are strict, payload-ref-only, and bind every registered profile exactly
(fail) HostExtensionPackageV1 artifact > every schema-reachable behavior derives exactly the registered capability vocabulary
(fail) HostExtensionPackageV1 artifact > generator --check is clean and detects a copied artifact drift
 86 pass / 4 fail / Ran 90 tests across 3 files

② 删掉新 profile 的 schema 文件 ⇒ 必须红

MUTATION 2: new profile schema file deleted
ENOENT: ... profiles/opencode-plugin.v1.schema.json
(fail) HostExtensionPackageV1 artifact > publishes the fixed path, sorted registries, and exact per-file SHA-256
(fail) HostExtensionPackageV1 artifact > schemas are strict, payload-ref-only, and bind every registered profile exactly
(fail) HostExtensionPackageV1 artifact > every schema-reachable behavior derives exactly the registered capability vocabulary
(fail) HostExtensionPackageV1 artifact > generator --check is clean and detects a copied artifact drift
 86 pass / 4 fail / Ran 90 tests across 3 files

③ 翻正向的那条断言自己是不是闸? —— 把新 profile 的 schemaPath 指向 skill 的 schema。profileId 列表仍然完全正确,:64 那条 exact-set 照绿:

MUTATION 3: new profile re-pointed at skill's schema (profileId list still exactly right)
-   "opencode-plugin@1 application/vnd.alpha...opencode-plugin.v1+json profiles/opencode-plugin.v1.schema.json",
+   "opencode-plugin@1 application/vnd.alpha...opencode-plugin.v1+json profiles/skill.v1.schema.json",
(fail) HostExtensionPackageV1 artifact > schemas are strict, payload-ref-only, and bind every registered profile exactly
 87 pass / 3 fail / Ran 90 tests across 3 files

⇒ 翻正之后的断言比它替换掉的反向断言更强:反向断言只管"这个词不出现",正向这条管完整绑定(id@version + mediaType + schemaPath)。

④ 拿掉脚本资产上界 ⇒ 边界对夹具必须红

MUTATION 4: script asset upper bound removed (falls back to maxPayloadBytes)
(fail) ... > script asset one byte over the registered maxScriptAssetBytes
(fail) ... > the script asset limit accepts exactly the registered value and refuses one byte more
 86 pass / 4 fail / Ran 90 tests across 3 files

四次实验都在本 worktree 内做、做完立刻还原;还原后 git status 干净、HEAD 未动、90 pass / 0 fail 复现。

alpha-check.sh 各步结果

▶ [1/7] north-star guard          ✓ zero upstream package edits
▶ [2/7] no literal NUL bytes      ✓
▶ [3/7] typecheck (×3 包)          ✓
▶ [4/7] contract lock + unit      ✗  ← 预期红,见下
▶ [5/7] assert gate files         ✗  ← 同一个红
▶ [6/7] seed assets               ✓
▶ [7/7] docs gate                 ✓ docs links (1 个 Markdown)

整轮只有两行 (fail),而且是同一条测试(一次在 [4] 的整包地板里,一次在 [5] 的逐文件点名里):

(fail) alpha-web extension package producer artifact pin > the vendored host contract copy is byte-identical to this repository's live host artifact
Expected: "57481e1a6a200d91a1559aebb5d3052ad933435e15bd17e59a28a85a35af41da"
Received: "5431b2cc2e03629d778af1715afcd08cf7ecb84a4abb66dab7b8d458bb63945c"

这就是基线 §6 硬序写死的那条反向 pin(packages/alpha-contracts-consumer/src/extension-package-artifact.test.ts:107-110):vendored 的宿主副本必须与本仓活的宿主 artifact 逐字节相同。T1 单独合并时它必红,这是设计如此。没有为了让它绿去动那条 pin,也没有动 vendored 副本。

本 PR 不单独合并。按基线 §6,T1 必须与 T4(re-vendor)在同一个 PR / 同一个 merge unit 里原子落地。

[4] 在 contracts-consumer 处 && 短路,ext 与 ui-mac 因此没在那一步里跑到。单独跑过,都绿:

packages/ext:     132 pass / 0 fail / Ran 132 tests across 10 files
packages/ui-mac:  3765 pass / 0 fail / Ran 3765 tests across 257 files

与 base fail-set 的差

base(本分支 base commit a828ec81,我自己在同一 worktree 实测)= ✅ all local gates green,fail-set 为空集。

⇒ 现在唯一的红全部是本 PR 引入的,而它的根因只有一个,且是 T4 的已知预期红。

一处订正:派发时给的 base 是 Ran 3756 tests across 255 files实测不是这个数。 我用 git checkout HEAD~1 -- <paths> 把工作树还原成 base 内容量了一次:

base:  3762 pass / 0 fail / Ran 3762 tests across 257 files
本 PR: 3765 pass / 0 fail / Ran 3765 tests across 257 files

+3,与我新增的三条断言(2 条 negativeCase + 1 条边界 test)逐条对得上,文件数不变。派发里的 3756/255 是个过期数字,不是本 PR 造成的差异。

主动没做的事

  • 没有做 T4 的 re-vendor,也没有碰 packages/alpha-contracts-consumer/vendor/** 或那条反向 pin —— 那是 T4 的边界,且票面明确要求这条红照实报。
  • 没有动 synthetic-decoder.ts。基线把它列在 T1 边界里,但实读之后它不需要改:它只读 maxPayloadBytesalpha.secret-prerequisite.v1,profileId 形参本来就是 PackageProfileIdV1,联合一扩就自动覆盖新成员。改它只会白白扰动 artifact 字节。
  • 没有把 decoder.ts 的 payload 分派重构成穷举 + 未知即拒。那个兜底式三元(else → decodeMcpRemotePayload)是基线 §5 第 7 类点名的对象,但那一类的票主是 T2b(咽喉在 package-admission.ts:506 的 kind 分流)。本票只按边界"加一臂"。基线 §2.4 也已实读定性:这里的兜底靠类型与 decoder.ts:406(supportComponent 具名拒绝 component-profile-unsupported)保证,它自己不是闸
  • 没有为 payload 加 targetDir / argv / environment 之类字段。载荷落在哪、引擎怎么被指过去,是宿主决定,不是 producer 声明 —— 那是 T2b 的事。
  • 没有新造 PackageAssetRefV1 之类的联合别名。两个具名类型够用,TS 在使用点自己推;为将来可能的消费方先造抽象是禁止项。

Fixes #807
Refs #699


R1 修复(commit 6052bb60)—— Codex 审计四条,全部采纳

artifactSha256(再次变化,以此为准):fb196fc1d187acb334b144374aeec2fe1c7da76f5b1bd4fac2df1c118ee0bba2

变了两次:F1 改文法一次、恢复 F1 的代码注释又一次(注释在 artifact 字节里)。
T3 要用的是这个最终值,不是正文上半部分的 f1fd0476…

F1(MAJOR)加 : 顺带放行了畸形 capability —— 已确认是真实回归

这条是我漏掉的。 派发时明确要我查「加 : 有没有顺带放宽别的东西」,我只验证了两个目标 token 能过、没验证别的什么也跟着能过。自己复跑确认:

"a::b"       package= accepted  leaf= skipped
"a:"         package= accepted  leaf= skipped
"engine:"    package= accepted  leaf= skipped
":x"         package= blocked   leaf= -
"a:b:c:d"    package= accepted  leaf= skipped

危险不在「多认了几个字符串」,而在:membership 只对 required 组件拒绝,optional 组件被 skipped 之后整包仍然 accepted —— 畸形值进来了,又哪里都不出现。

修法:两处都改成「原文法 精确的 engine:config|engine:plugin」,不留通用字符类。用程序比对而不是肉眼确认两处逐字一致:

decoder pattern : ^(?:[a-z][a-z0-9.-]{0,95}|engine:config|engine:plugin)$
schema  pattern : ^(?:[a-z][a-z0-9.-]{0,95}|engine:config|engine:plugin)$
byte-identical  : true
true   "alpha.connection.v1"    false  "a::b"      false  "engine:"        false  "engine:configx"
true   "engine:config"          false  "a:"        false  ":x"             false  "xengine:config"
true   "engine:plugin"          false  "a:b:c:d"

新负例的判据是「整包被拒」,不是「叶子被跳过」 —— 后者对畸形值恒真,杀不掉这个缺陷。放在 graphCases,其 runner 直接断 result.ok === false

绕过(把通用字符类放回去):

MUTATION F1: capability grammar reverted to the generic character class [a-z0-9.:-]
error: a malformed colon capability on an optional leaf blocks the whole package
(fail) ... graph refuses: a malformed colon capability on an optional leaf blocks the whole package
 85 pass / 1 fail / Ran 86 tests across 1 file

F2(MAJOR)plugin 的负向覆盖只有「量」没有「轴」

确认属实:超界与错 mediaType 都在讲资产取值;「缺字段」来自 agent(:205)、「未知 behavior key」来自 mcp-remote(:938),都是别的解码臂。补三条 plugin 专属具名负例(未知键 / 缺 asset / bytes 是字符串),保留原超界用例。

三次绕过,各自只打红自己那一条:

### F2-a: OPENCODE_PLUGIN_BEHAVIOR_KEYS 加 executeScript
(fail) ... plugin payload refuses an unknown behavior key          85 pass / 1 fail / Ran 86

### F2-b: 缺 asset 时不再要求它(退化成默认对象)
(fail) ... plugin payload refuses a missing asset                  85 pass / 1 fail / Ran 86

### F2-c: bytes 从字符串强转成数字
(fail) ... plugin payload refuses a string asset byte count        85 pass / 1 fail / Ran 86

### 全部还原后
 86 pass / 0 fail / Ran 86 tests across 1 file

F3(MAJOR)边界夹具分不清「接线到 registry」与「硬编码 2 MiB」—— 本轮最关键

审计说得对,而且我上一轮正文里把它当成已经解决的:我确实"从 registry 读期望值"了,但 registry 里钉的恰好也是 2 MiB,锚点和被测对象碰巧相等

修法:行为测试里临时把内存 registry 的界挪到 4096(既不是 maxScriptAssetBytes,也不是 markdown 5 MiB / payload 1 MiB 任何一条),验证边界跟着移动,finally 恢复并回断。决定性的一条是:界挪到 4096 之后,原来那个恰好合法的 2 MiB 必须变成越界。另加一条:opencode-plugin.v1.schema.jsonmaximum 必须等于 registry 的界(发布 schema 与宿主 decoder 是分开维护的两份,此前只查了 additionalProperties)。

绕过(把 decoder 写死成字面量 2097152):

MUTATION F3: decoder now hardcodes 2097152 instead of reading the registry
Expected to contain: "payload.behavior.asset.bytes: required integer in 1..4096"
(fail) ... the script asset limit accepts exactly the registered value and refuses one byte more
 85 pass / 1 fail / Ran 86 tests across 1 file

请特别注意这次绕过证明了什么:硬编码之下,原来那两条「恰好接受 / +1 拒绝」断言仍然全部通过 —— 是新加的「界挪走」那一段单独把它抓出来的。这正是审计指出的常量巧合盲区,上一版的闸确实是瞎的。

F4(MINOR)活跃合同索引发布着一个用不了的 pin

docs/contracts/host-extension-package-v1.md:191ed320… —— 查过来源:它在 284916c7(#729)时是真的,在 74af30d1(#749 合同 v2)之后就陈旧了。这份文档在本票之前就已经陈旧,不是我弄坏的,但也不该由我默默改掉。 已同步 SHA 与 last_reviewed,并在文中写明它是复述的散文、不是被校验的 pin,附上陈旧区间。

F4 的绕过问题:文档与 manifest 不一致时有没有东西会红?——没有。如实说没有。

两条检索轴:

  • 轴一「谁引用这份文档」:grep -a across *.ts/*.sh/*.py/*.yml/*.mjs/*.json零命中
  • 轴二「谁把 artifactSha256 与仓内别处比对」:命中全部落在 manifest ↔ vendored ↔ lock 之间,没有一处对 Markdown

还有一条执行出来的证据:该值从 #749 错到 #806,期间每道闸都是绿的 —— 包括我自己在 a828ec81 上跑的那次 ✅ all local gates green,当时文档正写着 1ed320…按指示没有为此新造闸门。

顺带一个自证:我这轮自己踩了一次同一个坑——恢复 F1 注释后 artifact SHA 又变了一次,而文档里还写着上一版的值;没有任何东西会红,只能手工核对:

doc      = fb196fc1d187acb334b144374aeec2fe1c7da76f5b1bd4fac2df1c118ee0bba2
manifest = fb196fc1d187acb334b144374aeec2fe1c7da76f5b1bd4fac2df1c118ee0bba2
MATCH ✓

R1 之后的门

▶ [1/7] north-star guard   ✓      ▶ [5/7] assert gate files  ✗ ← 同一个红
▶ [2/7] no literal NUL     ✓      ▶ [6/7] seed assets        ✓
▶ [3/7] typecheck ×3       ✓      ▶ [7/7] docs gate          ✓ (2 个 Markdown)
▶ [4/7] contract + unit    ✗ ← 预期红

整轮仍然只有两行 (fail),且是同一条测试(T4 的反向 pin,一次在 [4]、一次在 [5]):

(fail) alpha-web extension package producer artifact pin > the vendored host contract copy is byte-identical to this repository's live host artifact

没有动那条反向 pin,也没有动 vendored 副本。

[4] 在 contracts-consumer 处 && 短路,ext / ui-mac 单独跑:

packages/ext:     132 pass / 0 fail / Ran 132 tests across 10 files
packages/ui-mac:  3769 pass / 0 fail / Ran 3769 tests across 257 files

与 base(实测 3762 / 257)的差 = +7,与我的枚举逐条对得上:R0 加 3 条(2 negativeCase + 1 边界 test)、R1 加 4 条(1 graphCase + 3 negativeCase)。文件数不变。

R1 里我踩到、并且抓住了的一个本机陷阱

绕过实验用 git checkout HEAD -- decoder.ts 还原时,把同一文件里我刚写、尚未提交的注释一起抹掉了 —— 代码是修好的 alternation,注释却退回旧版、还在说「widening the character class by one byte」,即注释在说谎而没有任何测试会红。靠 git diff 逐行看出来的,不是靠印象。这正是 CLAUDE.md 记着的那条(实验后 git checkout -- 会连未提交的真实改动一起抹掉),这次的变体是「同一个文件里,一半是实验一半是真改动」。

审计没点到、我这轮另外发现的一条(没有改)

同一份 docs/contracts/host-extension-package-v1.md:40-41 写着消费侧闭包是「four profiles, one capability, cloud and Alpha Connection absent」。这句今天也是假的:消费侧测试实际断言的是三个 capability,且 Alpha Connection 早在合同 v2(#749)就转正了。

我没有改它,两个理由:①它描述的是 producer/consumer 闭包,票主是 T3 / T4,不是 T1;②那句话在 T4 re-vendor 之后还要再变一次(profile 变五个),现在改等于猜一个尚未发生的状态。登记在此,请编排者决定挂给谁。


T4(#811,commit 533a2551)—— re-vendor producer artifact + lock + closure

本 PR 现在同时闭合 #807(T1)与 #811(T4)。 基线 §6 把两者钉成同一个 merge unit:
extension-package-artifact.test.ts:110 要求 vendored 宿主副本与本仓活的 host artifact 逐字节相同,
所以 T1 单独在时本仓自己的正式门就是红的。上面那条「预期红」现在绿了。

大白话

把 alpha-web 重生后的 producer artifact 逐字拷回本仓的 vendored 副本,更新 lock 与 closure 断言,
并把落后一个 commit 的那批资产字节一起前移。

bash scripts/alpha-check.sh —— 全绿

▶ [1/7] north-star guard          ✓ zero upstream package edits
▶ [2/7] no literal NUL bytes      ✓
▶ [3/7] typecheck (×3 包)          ✓
▶ [4/7] contract lock + unit      ✓ tests
▶ [5/7] assert gate files         ✓ 79 个闸门文件全部在位且真的跑过
▶ [6/7] seed assets               ✓
▶ [7/7] docs gate                 ✓ docs links (4 个 Markdown)

✅ all local gates green — safe to push (alpha-ci will mirror this).

[4] 不再短路,三个包都真的跑到了:

verified 39 contract artifacts from jinjunnn/alpha-web@9fcd83d66ea8f5b13081f434ace150e739a0536e (lock + staged upstream bytes)
packages/alpha-contracts-consumer:  54 pass / 0 fail / Ran 54 tests across 5 files
packages/ext:                      132 pass / 0 fail / Ran 132 tests across 10 files
packages/ui-mac:                  3769 pass / 0 fail / Ran 3769 tests across 257 files

推送时 pre-push 钩子在同一棵树上又整跑了一遍,同样 ✅ all local gates green

与 base fail-set 的差

base = 本分支 T1 的 HEAD 6052bb60,工作树干净,我自己实跑:

base(6052bb60) 本 commit(533a2551)
[4] contracts-consumer 53 pass / 1 fail / Ran 54 tests across 5 files 54 pass / 0 fail / 5 files
[4] ext 没跑到(&& 链在 consumer 处短路) 132 pass / 0 fail / 10 files
[4] ui-mac 没跑到(同上) 3769 pass / 0 fail / 257 files
[5] gate files ✗(同一条测试的第二次执行)
整轮 (fail) 行数 2 行,同一条测试 0

那条唯一的红是:

(fail) alpha-web extension package producer artifact pin > the vendored host contract copy is byte-identical to this repository's live host artifact
Expected: "c9148c6f00f025b0f679a2d63ffb71d4f64ad43f33478c37271997a5815975ff"   ← 本仓活的 host artifact
Received: "5431b2cc2e03629d778af1715afcd08cf7ecb84a4abb66dab7b8d458bb63945c"   ← 旧 pin 里的宿主副本

re-vendor 之后 Received 侧变成 c9148c6f…,两边相等 ⇒ 这条转绿。这就是本票的成功判据。
另有一条新红是我自己引起并当场修掉的:scripts/vendor-lock-degradation.test.ts:94-95 把旧 commit
写成字符串断言,pin 一动它就红(这正是它该有的行为),同步改成新 commit / 新条数。

逐文件 sha256 比对

不用 vendor 脚本自己的输出当判据(那是自指等价链)。独立拿 git -C ../alpha-web show <commit>:<path>
的字节 + shasum -a 256 跑一遍:

upstream files in git@9fcd83d66ea8f5b13081f434ace150e739a0536e :       39
vendored files on disk                                         :       39
SET: identical
--- per-file sha256 (git object bytes vs vendored bytes) ---
compared 39 files, 0 mismatches

这套比对自己先证明能测出已知的坏:同一个循环拿旧 pin 6e0db57d 的字节再跑一遍,

negative control: compared 36 files against OLD pin, 19 mismatches (must be > 0)
+ ABSENT-in-OLD × 3(asset.generic-plugin.js / expected.plugin.compiled.json / input.plugin.valid.json)

19 + 3 = 22,与 git diff --stat 6e0db57d 9fcd83d 的 22 个文件逐一对上。

38 vs 39:两个数不一样,原因是结构性的

来源 是什么
generator 输出 ✓ … (39 files) / ls / lock files[] 39 目录里全部文件
extension-package-producer-artifact.v1.jsonfiles[] 38 全部文件减去 manifest 自己

程序核对:set(disk) - set(manifest.files) == {"extension-package-producer-artifact.v1.json"},反向为空集。
manifest 装不下自己的哈希,这与它 producerCommit: {embedded: false} 是同一个理由的两半。
这个「差一」没有写成第二个魔数:extension-package-artifact.test.ts:58-60 断言
lock.files 路径集 == manifest.files 路径集 ∪ {manifest 自己},lock.files.length 单独钉 39。
(上一版 lock 是 36、manifest 35,同一个关系。)

closure exact-set:两个方向各绕过一次,外加一次反事实

控制组(未改动,只跑这一条):1 pass / 6 filtered out / 0 fail

方向 A — 悄悄多一个。 往 vendored registry 与 generic-rules.v1.json 同时加一个未经宿主登记的
engine:shell(两份保持互相自洽,单靠「两份互相当判据」抓不到):

expect(registry.capabilities.map(...)).toEqual([
error: expect(received).toEqual(expected)
@@ -6,3 +6,3 @@     "engine:plugin",
+   "engine:shell",
(fail) ... v2 profile/capability closure ...        0 pass / 1 fail

方向 B — 悄悄少一个。 从 registry / rules / profiles 三份一起删掉 engine:plugin
opencode-plugin(三边一起变小并保持自洽):

expect(registry.profiles.map(...).sort()).toEqual([
@@ -4,3 +4,3 @@     "mcp-remote",
-   "opencode-plugin",
(fail) ... v2 profile/capability closure ...        0 pass / 1 fail

方向 C — excluded 表悄悄多一个(本 PR 把它从 arrayContaining 收成 exact-set 的理由)。
managed-plugin 塞回排除表:

expect(rules.excluded).toEqual([
@@ -3,3 +3,3 @@     "legacy-projection",
+   "managed-plugin",
(fail) ... v2 profile/capability closure ...        0 pass / 1 fail

反事实(证明这次收紧是承重的,不是装饰):同一个变异不动,只把断言换回旧的
expect.arrayContaining([...]) 形状 ——

 1 pass / 6 filtered out / 0 fail

⇒ 旧写法下,「上游把 managed-plugin 重新塞回排除表、让刚转正的 profile 静默失效」不会红

三次实验都在本 worktree 内做、git status 干净时开始、做完立刻 git checkout HEAD -- 还原,
还原后 git status 空、git diff HEAD 无输出、HEAD 未动、7 pass / 0 fail 复现。

aw#112 frontmatter 变化牵动的宿主测试语料 —— 逐条,不混在 profile 变更里

上游那一跳到底改了什么(aw@e614b5e,[#112][CODE] canonical 语料资产补 frontmatter):

-# Generic bundle agent                 +---
-                                       +name: generic-bundle-agent
-Deterministic corpus asset.            +description: Generic bundle agent
                                        +mode: primary
                                        +---
                                        +
                                        +Deterministic corpus prompt body.

(skill 同形,少一行 mode。52 → 118 字节 / 52 → 103 字节。)

宿主语料 它读什么 这次改了吗 为什么
src/main/package-admission.test.ts expected.mcp-remote.compiled.json 没改 它整份从产物现算:payload 字节从产物自己的 payloads 映射重序列化、摘要现算。那份产物本轮只动了 compatibility.reportSha256 一行,而它不含 markdown 资产,frontmatter 与它无关。
src/main/package-update.test.ts expected.bundle.compiled.json 没改 它只借产物的图形状,资产字节用自己的 AGENT_V1/V2SKILL_MD(本来就带 frontmatter),每代的 payloadRef / manifestDigest / owner token 全部现算。产物里那些 bytes: 375→376sha256assetManifestSha256 的变动它一个都没硬编码。
test-component/ext-package-detail-wiring.cases.ts vendor 目录 没改 同上,现算。
test-component/package-mixed-bundle.fixture.ts expected.bundle.compiled.json 改了,但只改注释 见下。

交叉验证「真的没有硬编码」,而不是靠读代码的印象:把旧语料里五个会变的哈希 + 旧 commit
拿去全仓 grep -rna -a(带 -a,防 NUL 假阴):

81353065…(旧 agent md)    → 零命中
d3e1d121…(旧 skill md)    → 零命中
736b6f02…(旧 agent payload) → 零命中
6228388d…(旧 skill payload) → 零命中
2a0faedb…(旧 compat report) → 零命中
d229717f…(旧 producer aggregate) → 零命中
6e0db57d…(旧 pin commit)   → 零命中

四份语料里三份一行未动,是因为它们从 vendored 字节现算,不是因为断言太松没看见。

package-mixed-bundle.fixture.ts:改的是理由,不是字节

该文件抬头原文写着:vendored 的两份 markdown 没有 frontmatter,宿主
(agentMdToEntry--- + description + 非空 body;skillGenerationProbe 要 frontmatter name
等于 item key)因此装不进去,所以夹具手工重造了资产字节;并把修复挂在 aw#108

aw#112 已经把这个缺口关了,而且关得很彻底 —— 夹具里那两串常量逐字节等于新的 vendored 资产:

AGENT_MD  fixture 118 f2a5576d98928e8df98f4608ac8521cabd95b17875a474317193836584683c65
AGENT_MD vendored 118 f2a5576d98928e8df98f4608ac8521cabd95b17875a474317193836584683c65   identical: true
SKILL_MD  fixture 103 4e62da1a8bc554708127ec4aa749e77850462478efd378338c56da6f348ba38e
SKILL_MD vendored 103 4e62da1a8bc554708127ec4aa749e77850462478efd378338c56da6f348ba38e   identical: true

(这两个值也正是新 expected.bundle.compiled.json 里那两个 behavior.asset.sha256。)

注释照原样留着就成了谎话 —— 而没有任何测试会因为它说谎而红,正是 CLAUDE.md 里那条。
所以改写为实况,并明写两件事:①夹具仍然自建信封,理由换成了两条与 frontmatter 无关的
(第四格「已策展但宿主不支持」的 optional leaf、breakSkillFrontmatterName 开关,上游产物里都没有);
这份字节等同没有任何断言看着 —— assertMatchesVendoredBundleShape 只比形状,别把它读成
「字节也验过了」。同一理由也补进那个函数的 doc comment。

③ 合同索引 docs/contracts/host-extension-package-v1.md 四项手工核对

没有任何代码 / 脚本 / CI 读这份 Markdown —— 两条独立检索轴(轴一:谁引用这个文件路径;轴二:
谁把 artifactSha256 与仓内别处比对)在 T1 那轮已经跑过,命中全部落在 manifest ↔ vendored ↔ lock
之间。实证是那个值从 #749 一路错到 #806 期间每道闸都绿
既然没有自动判据,手工核对的过程就是证据;按指示没有为此新造闸门。

# 文档原文 真实 manifest 处置
profile 数 「four profiles」 registry 5 个:agent / mcp-local / mcp-remote / opencode-plugin / skill;generic-profiles.v1.json 的 mappings 同 5 个 改成五个并逐个点名
capability 数 「one capability」 5 个:alpha.connection.v1 / alpha.mcp-oauth.v1 / alpha.secret-prerequisite.v1 / engine:config / engine:plugin(generic-rules.v1.jsoncapabilities 与 registry 逐字相同) 改成五个并逐个点名
Alpha Connection 「Alpha Connection absent」 在册,#749 就转正了 改成「is a registered capability — promoted in #749」;cloud 仍在排除表,这半句保留
current SHA producer pin 写着 alpha-web@b71748103ce65f97e3e5c8ac03f08152a0a1456f + aggregate ae9f43cc… lock / manifest 都是 9fcd83d66ea8f5b13081f434ace150e739a0536e + 2a36d9cb… 前移;并补一句它与上面那个 host aggregate 一样是转述散文、跨 #759 一直陈旧而每道闸全绿

顺带把 excluded 也写进去(cloud / legacy-projection / nested-bundle / provider-adapter /
publish-wrapper),并注明 managed-plugin 是在 #807 注册 opencode-plugin 时离开的 ——
消费侧那条断言现在是 exact-set,文档与它一一对应。

文首那个 host aggregate fb196fc1… 本轮核对过、无需改动:与
packages/ui-mac/src/shared/host-extension-package-contract/host-extension-package-artifact.v1.json
artifactSha256 逐字相同(T1 的 R1 已对齐),也与 producer 侧 host-contract-pin.v1.json
generic-rules.v1.json.hostContract 三方一致。

主动没做的事

  • 没有给「夹具资产字节 ≡ vendored 资产字节」加断言。 这个等同今天成立、且没有任何闸看着,
    上游下次动资产内容它会安静地退回一份手抄替身。但那是新加一道闸,不在 re-vendor 的边界内
    (票面「禁止过度工程:不重构、不加闸」)。已把这件事写进夹具抬头,不让它变成隐性知识。
    建议单开一张窄票。
  • 没有改那条测试的标题。 它现在还覆盖了 opencode-plugin 一档,标题却只提 OAuth / Alpha
    Connection。原因:这个字符串是
    docs/verification/2026-07-31-req128-capability-matrix/matrix.tsv:7 的 evidence key,由
    packages/ui-mac/src/main/req128-capability-matrix.test.ts 反查 —— 我先改了名,那条自闸当场红
    (+ "…extension-package-artifact.test.ts :: v2 profile/capability closure covers OAuth and Alpha Connection and still excludes cloud"),说明改名要动一份已归档的 Phase 1 验证证据
    已还原并在测试上方注明理由。
  • 没有碰宿主实现、renderer、alpha-web 任何一行(#809 / #810 / T3 的边界)。
  • 没有动 packages/ui-mac/src/shared/host-extension-package-contract/** —— T1 已经在这个 PR 的
    上半场处理完,T4 只消费它的字节。

顺手发现、没有改、请编排者决定挂给谁

同一份 docs/contracts/host-extension-package-v1.md 结尾第 88-89 行写着:

Phase 1 has one required component, so the wire does not reserve optional-child or Alpha
Connection states.

「不保留 optional-child 状态」这半句今天是假的。 packages/ui-mac/src/shared/catalog-package-view.ts:30-36
CatalogPackageComponentV1 逐组件带 included: boolean
skipReasonCode: PackageComponentSkipReasonV1 | null(注释原文:「included is the only authority on
whether a component takes part in the install」),而 CatalogPackageViewV1.components 是一行一个组件。
Alpha Connection 那半句我没有查证,不猜。

没有顺手改的理由:①它讲的是 renderer wire,票主更像 #810,不是本票的 re-vendor;
②只修一半会写出一句新的半真话。登记在此。

⚠️ 推送时发生的一次真实事故(已修复,但根因还在,需要单开票)

git push 走 pre-push 钩子(= alpha-check.sh)之后,远端分支尖端不是我提交的那个 commit:

533a2551..e33147ed  feat/807-opencode-plugin-profile -> feat/807-opencode-plugin-profile

多出来的三个 commit 长这样(reflog 显示它们是在钩子跑测试的那一刻产生的):

e33147ed  Test <test@opencode.test>  Mon Aug 3 03:38:20 2026  root commit /private/var/.../opencode-test-hyugxekp48l
d5394029  Test <test@opencode.test>  Mon Aug 3 03:38:20 2026  root commit /private/var/.../opencode-test-0g5p7epo4kx8
50583b51  Test <test@opencode.test>  Mon Aug 3 03:38:19 2026  root commit /private/var/.../opencode-test-3jyb2i7plwc
git diff --stat 533a2551 e33147ed   →  (无输出:三个 commit 零文件改动)

根因是 #754 那一类的另一个实例。 git push 会把 GIT_DIR 导出给钩子进程(linked worktree
下实测必有),而 GIT_DIR 打得过 -C / cwd。上游测试夹具
packages/opencode/test/fixture/fixture.ts:93 正是
$`git commit --allow-empty -m "root commit ${dirpath}"`.cwd(dirpath) —— 它以为自己在临时目录里
建 fixture 仓,实际把空提交打进了本仓当前分支

当场复刻(不是推断):A、B 两个空仓,cwd=A 但环境里带 B 的 GIT_DIR——

A commits before: 0     B commits before: 1
A commits after : 0     B commits after : 2
B tip           : 861aa65 root commit …/gitdir-demo/A

处置:git reset --hard 533a2551 + --force-with-lease=…:e33147ed 强推回真实尖端,
本次强推用 --no-verify —— 理由不是「树被别人占着」,而是钩子自己就是肇事者,再跑一次会再造三个。
现在 origin/feat/807-opencode-plugin-profile == 533a2551,工作树干净,PR head 已回正。

没有顺手修根因:肇事文件在 UPSTREAM_PATHS 下(packages/opencode/**),alpha 不碰;
真正的修法应该落在钩子侧(照 packages/alpha-contracts-consumer/scripts/git-cross-repo.ts 已经
做过的那样,用 git rev-parse --local-env-vars 清掉仓库局部变量再跑测试)。建议单开一张票。
#809(T2b)从本分支派生的角度看还有一条:如果它已经 fetch 到 e33147ed,rebase 前需要
重新 fetch —— 那三个 commit 已经不在远端了。

Fixes #811

jinjunnn added 3 commits August 3, 2026 02:09
…in 转正为 host capability

第五种载荷:会在引擎进程内以同权限执行 JS 的 managed OpenCode Plugin。前三相不是忘了做,
是主动立了三道 exact-set + 一条反向断言挡住它悄悄进来;本票把那条反向断言**翻成正向**
(只删掉它 = 少一道闸),并把 registry 恰含哪五个 profile 连同 mediaType/schemaPath 一起钉住。

能力**不新造词**:`engine:config` / `engine:plugin` 是 renderer 授权屏对 legacy 插件安装
一直在用的两个词(shared/ext-capability-authorization.ts:30 今天就同时申请这一对),直接登记
进 host registry。只派生一个新词会让 managed 路径比 legacy 路径**披露得更少** —— 用户看不到
「这东西会写引擎配置」。host token 只靠 registry membership 判定,无 `alpha.*` 命名约束。

载荷是单个内容寻址的 JS 资产:`mediaType: "text/javascript"`(判别式,不放宽成 string)+
独立的界 `maxScriptAssetBytes = 2 MiB`,刻意与 markdown(5 MiB)和 payload(1 MiB)都不同值。
不引入 bundler、不改写第三方字节。

基线连锁表未点名、实跑才暴露的两处:
- `CAPABILITY_RE` 与信封 schema 的 capability pattern 都是 `^[a-z][a-z0-9.-]{0,95}$`,**不含冒号**
  ⇒ `engine:config` 结构上进不了信封。两处同步加 `:`。它只是文法/DoS 界,真正的准入是
  `isPackageCapabilityV1` 的 registry membership,故放宽一个字节不放宽准入面。
- `req128-capability-matrix` 按测试标题钉死证据,重命名那条断言后必须同步 matrix.tsv。

Fixes #807
Refs #699
F1(MAJOR,真实回归):上一版把 capability 文法放宽成通用字符类 `[a-z0-9.:-]`,顺带把
`a::b` / `a:` / `engine:` / `a:b:c:d` 一起判成合法。危险的不是"多认几个字符串",而是它们挂在
**optional** 叶子上时:membership 让叶子 skipped,而整包仍判 accepted —— 畸形值进来了又哪里都
不出现。实跑复现:optional leaf 的 capability 改成 `a::b` ⇒ `package=accepted / leaf=skipped`。
改成"原文法 或 精确的 engine:config|engine:plugin"两选一,`decoder.ts` 与信封 schema 两处
**逐字一致**(已用程序比对,不靠肉眼)。补一条 graphCase:判据是**整包被拒**,不是叶子被跳过。

F2(MAJOR):新 profile 的负向覆盖只有"量"没有"轴" —— 超界与错 mediaType 都在讲资产取值,
而"未知键 / 缺字段 / 类型错"三条轴此前借的是 agent 与 mcp-remote 的用例,**是别的解码臂**。
给 OPENCODE_PLUGIN_BEHAVIOR_KEYS 加一个键、或把 bytes 强转成数字,原断言全绿。补三条
plugin 专属具名负例。

F3(MAJOR,本轮最关键):边界夹具虽然从 registry 读期望值,但 registry 里钉的**恰好也是** 2 MiB
⇒ 把 decoder 写死成字面量 2097152 仍满足全部断言。比较基准与被测对象碰巧相等 = 闸是瞎的。
改成**把界挪到 4096、验证边界跟着走**,并断言"原来那个恰好合法的 2 MiB 现在必须越界";
另断言 opencode-plugin schema 的 `maximum` 等于 registry 的界(发布 schema 与宿主 decoder
是分开维护的两份,此前只查了 additionalProperties)。

F4(MINOR):`docs/contracts/host-extension-package-v1.md` 的 current SHA 是 `1ed320…` ——
它在 `284916c7`(#729)时为真,`74af30d1`(#749 合同 v2)之后就陈旧了,**在本票之前**。
仓内没有任何东西读这份文档,所以那段期间每道闸都是绿的。同步 SHA 与 last_reviewed,
并在文档里写明"这是复述的散文,不是被校验的 pin",让下一个人别再信它。

Refs #807
…ile / 五 capability,并把 aw#112 的资产字节前移

T3(alpha-web#129)重生的 producer 产物落到消费侧。语料 36 → 39:`input.plugin.valid.json`、
`expected.plugin.compiled.json`、以及 plugin 组件引用的脚本资产 `asset.generic-plugin.js`
(本仓 vendor 清单里第一份 `.js` 字节,与两份 `.md` 同走非 JSON 分支)。一个文件都没被撤掉,
所以 vendor 「只写不删」这次没留下无主残留;目录清单断言仍是那条判据。

**这一跳与 `#807` 必须原子落地**:`extension-package-artifact.test.ts:107` 要求 vendored 宿主
副本与本仓活的 host artifact 逐字节相同。`#807` 单独在时那条恒红(实测 base:consumer
53 pass / 1 fail,step [4] 的 `&&` 链就此短路,ext 与 ui-mac 那两步根本没跑)。re-vendor 之后
`bash scripts/alpha-check.sh` 全绿。

closure exact-set(`:145-196`):
- profile 五个(加 `opencode-plugin`),registry 与 `generic-profiles.v1.json` 两份各断一次;
- capability 五个(加 `engine:config` / `engine:plugin`),`generic-rules.v1.json` 与 registry 互相当判据;
- `rules.excluded` 从 `arrayContaining` 收成 exact-set。`managed-plugin` 离开排除表是 Phase 4 的
  合同事实,而 arrayContaining 只挡「悄悄少一个」—— 上游把某一档重新塞回排除表正是让已转正的
  profile 静默失效的走法,那个方向此前不红;
- `vectors.valid` 五份(加 `input.plugin.valid.json`),五份都要过发布的声明 schema。

lock 39 条、manifest 38 条:manifest 装不下自己的哈希(`producerCommit.embedded: false` 是同一
理由的另一半),这个「差一」不写成第二个常数,由 `lock.files == manifest.files ∪ {manifest}`
那条 `toEqual` 对死。

`aw#112`(frontmatter)带来的宿主侧影响,单独记在这里:四份被点名的宿主语料
(`package-admission.test.ts` / `package-update.test.ts` / `ext-package-detail-wiring.cases.ts` /
`package-mixed-bundle.fixture.ts`)**一行代码都没改** —— 它们全部从 vendored 字节现算摘要,
没有任何硬编码的 sha,已用五个旧哈希 + 旧 commit 全仓 `grep -a` 交叉验证为零命中。
真正变化的是 `package-mixed-bundle.fixture.ts` 抬头那段**理由**:它当初手工重造资产字节,是因为
上游两份 markdown 没有 frontmatter、宿主装不进去;`aw#112` 已经把这个缺口关了,而 fixture 里那
两串常量如今与 vendored 资产**逐字节相同**(118 / 103 字节)。注释照原样留着就成了谎话,故改写
为实况,并明写「这份等同没有任何断言看着」—— 加那道闸不在本票边界内。

`docs/contracts/host-extension-package-v1.md` 四项对齐(手工核对,没有任何代码读这份 Markdown,
所以它陈旧时不会有东西变红):producer pin commit / aggregate 从 `b7174810` / `ae9f43cc` 前移到
`9fcd83d6` / `2a36d9cb`(它跨过 `#759` 一直陈旧而每道闸全绿),profile 四 → 五、capability 一 → 五、
「Alpha Connection absent」翻正(`#749` 就已转正),并补一句它和上面那个 aggregate 一样是转述散文。

`v2 profile/capability closure …` 这条测试的**标题保持不动**:它是
`docs/verification/2026-07-31-req128-capability-matrix/matrix.tsv:7` 的 evidence key,
改名会去动一份已归档的 Phase 1 验证证据。

Fixes #811
Refs #699
@jinjunnn
jinjunnn force-pushed the feat/807-opencode-plugin-profile branch from e33147e to 533a255 Compare August 3, 2026 07:41
@jinjunnn
jinjunnn merged commit 6126e69 into alpha Aug 3, 2026
6 checks passed
@jinjunnn
jinjunnn deleted the feat/807-opencode-plugin-profile branch August 3, 2026 08:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant