fix(metadata-protocol): 对象 overlay 写路径的 ownership 键切真实 package id + 服务端强制 provenance 盖章 (#4636 裁 B 第一步) - #6219
Merged
Conversation
…务端强制 provenance 盖章 `applyObjectRegistryMutation` 此前把每一次对象写入都硬编码登记在 `'sys_metadata'` 哨兵下。该归属键同时就是包过滤键(`getAllObjects(packageId)` 匹配的是 `contributor.packageId`),所以通过 Studio 包工作区新建的对象在自己所属包的过滤结果 里一直是空的;boot 侧(PR2)也无法单独改成真实 id —— 两侧会为同一个对象互相抢 ownership,`registerObject` 在第二次认领时抛 `already owned by package …`。 改动三处: - `applyObjectRegistryMutation` 接受 `packageId`,用 `request.packageId || 'sys_metadata'` 作归属键(哨兵只留给「没有绑定包」的写入); - 服务端在**副本**上无条件盖 `_provenance: 'org'`,不采信请求体; - `rollbackMetaItem` 从行本身(`resolveOverlayPackageBinding`)读出绑定后再写透, 而不是从请求读 —— 请求上没有这个参数,凭空加一个等于允许调用方重设自己不拥有的 对象的归属。 盖章与切键必须同一个 PR:只切键会立刻复活 cloud#970。`applyProtection` 会把带包 id 且自身没有 provenance 的 body 默认标成 `'package'`,`getArtifactItem` 据此判定它是 代码制品,而 `object` 声明了 `allowOrgOverride: false`,于是用户刚建好的对象在下一次 保存时收到 403 not_overridable。新增测试里的 B-minimal 反例在真 SchemaRegistry 上把 这条因果链钉住了。 Part of #4636 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
…tepath-real-package-id
回滚的 package-bound 分支为何今天不可达,原本只写在 PR 正文里;把 issue 号写进 测试本体,读到这条 tripwire 的人不必回翻 PR 就知道它钉的是哪个缺陷。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 15 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
baozhoutao
marked this pull request as ready for review
August 7, 2026 11:20
This was referenced Aug 7, 2026
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.
Part of #4636 — 维护者 2026-08-07 裁决 B 的第一步:写路径半边。boot 半边(
loadMetaFromDb)按裁决的次序论证留给 PR2,本 PR 不触;因此正文不写Fixes,#4636 留给 PR2 关闭。问题
applyObjectRegistryMutation把每一次对象 overlay 写入都硬编码登记在'sys_metadata'哨兵下。这个归属键同时就是包过滤键 ——SchemaRegistry.getAllObjects(packageId)匹配的是contributor.packageId(registry.ts:1252),runtimemeta.ts的侧边栏包过滤直接消费它。于是通过 Studio 包工作区新建的对象,在它自己所属包的过滤结果里是空的,直到有别的路径把它重新登记一遍。也正因为如此,boot 侧不能单独修:两侧一旦一个用
app.{slug}、一个用'sys_metadata',registerObject会在第二次认领时抛already owned by package …—— 这正是前任 dev 实测出来、并让本单转维护者的那条死路。改了什么
三处,都在写路径:
applyObjectRegistryMutation接受packageId,归属键改为request.packageId || 'sys_metadata'。哨兵只留给「没有绑定任何包」的写入。用||而不是??:空串绑定就是「没有包」,与 boot 分支对package_id的归一化写法一致。上游applyRegistryWriteThrough的两个调用点(saveMetaItem、runPublishSideEffects)本来就带着packageId,只是没往下传。_provenance: 'org',不采信请求体。rollbackMetaItem从行本身读出绑定(新增resolveOverlayPackageBinding)再写透。rollbackMetaItem请求上没有packageId参数,凭空加一个等于允许调用方重设自己并不拥有的对象的归属;行的package_id才是权威答案。读在 restore 之前做:此刻行还在,读失败可以干净地让整个回滚失败,而不是把一个会失败的查询摆在已经成功的写入后面([metadata-protocol] SysMetadataRepository 的 nextEventSeq()/nextItemVersion() 同样把读失败当「表还没建」,静默从 1 重新发号 —— #4825 在 canonical 路径上的同形缺陷 #4867 那种catch {}吞掉真实故障的形状)。为什么盖章必须和切键同一个 PR
只切键会立刻复活 cloud#970。因果链是机械的:
applyProtection(spec)拿到一个包 id、而 body 自己没有_provenance时,默认盖'package'→getArtifactItem读的正是这个键 →isArtifactBacked变真 → 而object声明了allowOrgOverride: false→saveMetaItem的 overlay 门对下一次保存回403 not_overridable。用户刚建好的 app 变成静默不可编辑,只是从「重启一次之后」提前到「保存一次之后」。客户端的 provenance 在这里不可信:
metadata-read-decorations.ts有意不剥离_provenance,Studio 的 GET → PUT 往返会把服务端自己发出去的值原样送回来 —— 采信它等于采信自己的输出。所以服务端陈述这个事实,而不是读回来。这与 boot 重水合对同一批行早就在写的那句话完全一致。前提复核(在
origin/main上)applyObjectRegistryMutation仍硬写'sys_metadata'protocol.ts:7362(立单时 :7220,行号已漂移):registerObject(request.item as any, 'sys_metadata')403 not_overridable当场复现registry.ts契约注释的 save-path 半句P3 细节:注释原文「That sentinel only holds on the save path」在本 PR 之后失真 —— save path 现在只对「无包绑定」的写入用哨兵。已就地最小改写成「该哨兵只标记一件事:这行没有绑定任何包」,并把 boot 半句显式标注为「描述的是契约,还不是代码」(PR2 落地后即可去掉该标注)。注释的结论(
_packageId !== 'sys_metadata'无法回答 provenance)不但没变,而且更成立了:切键之后连 save path 的包绑定行也带真实 id,哨兵判据更不可靠。注释本体的完整同步仍归 PR2。测试
新增
packages/objectql/src/protocol-writepath-object-ownership.test.ts(10 例)。放在 objectql 而不是被测代码旁边,是因为断言的对象是真SchemaRegistry(ownership 冲突、applyProtection的 provenance 默认、getAllObjects过滤);objectql 依赖 metadata-protocol,只有这个方向能同时握住两半,反向 import 会闭合 turbo 拒绝的环。packageId→ contributor 记app.myapp;不带 → 哨兵照旧。_provenance→ 记'org';body 回传_provenance: 'package'→ 仍记'org'(请求永不胜出);且盖在副本上,调用方 body 不被写回_packageId/_provenance。due_date进内存 schema,落盘仍是一行、仍绑在包上。403 not_overridable,并断言_provenance被默认成'package',把因果链本身钉住。getAllObjects('app.myapp')首次保存即返回该对象,且不串到app.otherapp。门:
packages/objectqlpnpm typecheck通过。metadata-protocol没有typecheck脚本(在 type-check-coverage 的 DEBT ledger 里),直接跑tsc --noEmit得到 63 条既存错误,全部在该包的*.test.ts里,src/protocol.ts零错误 —— 基线状况,非本 PR 引入。反向验证(方向先写死,再实测)
dist/,不是 src。第一次跑肢 A 时忘了重建,得到「全绿」的假阴性;两条肢的下列结果都是重建之后的。[not_overridable] Metadata item 'object/myapp_invoice' is provided by a code package …;另expected 'package' to be 'org'expected 'sys_metadata' to be 'app.myapp';包过滤expected [] to deeply equal [ 'myapp_invoice' ]肢 A 多出来的 4 例不是预测落空,是同一个 403 的下游:盖章一去,第一次保存就把对象标成代码制品,于是两条 provenance 断言直接红,两条回滚用例连第二次保存都做不到(其中包括 tripwire 那条,它拿到的是
not_overridable而不是预期的metadata_conflict)。按「模板是工具不是真相」,如实记下范围差,而不是把它修剪成预测的样子。另一处如实记录:肢 B 没有覆盖到回滚那条改动 —— package-bound 回滚在两种形态下都 409(见下),package-less 回滚两种形态下都走哨兵。回滚这一笔的正确性由「键从行读出而不是从请求读」的论证 + package-less 端到端用例承担,不由肢 B 承担。
途中发现,已立单不修:#6215
rollbackMetaItem/revertCommit对任何 package-bound overlay 行必定 409,与本 PR 无关(sys-metadata-repository.ts一个字都没动)。restoreVersion调put时不传packageId,put又用whereFor(ref, state, opts.packageId ?? null)——null是谓词(package_id IS NULL)而不是「任意包」,于是它找不到自己正在恢复的那一行,parent 读成 null:后果:本 PR 回滚改动的 package-bound 分支今天不可达。我没有顺手修它(不在裁决范围,且改的是仓库写路径,需要自己的 pin 测试和 review),而是留了一条 tripwire 用例断言当前的 409 并指向 #6215 —— #6215 修好那天这条用例会红,修的人必须接着断言 ownership 键,而那句断言就写在它上一条用例里。
必答项
#5079(
removeRuntimeShadow)重新定价 —— 重测结论:治愈条件语义没有变化,成本模型不变;#5079 不应被定价成「等 #4636」。探针(真 SchemaRegistry,复现写路径改动后的两次调用,带对照组):
机制:
removeRuntimeShadow扫的是this.metadata.get(type)里的复合键pkg:name兄弟项,判据it._packageId !== 'sys_metadata'也是对那个 Map 里的条目求值。而 object 这一片永远只有裸键:applyObjectRegistryMutation调registerItem(type, item, 'name')不带 packageId(裸键),而全仓没有任何路径以 packageId 调registerItem('object', …)——plugin.ts:1908对type === 'object'显式return(注释:objects are registered differently (ownership model))。所以复合兄弟项从不存在,循环根本走不到那个判据。本 PR 搬的是objectContributors的键,是removeRuntimeShadow从不触碰的另一个结构。对照组证明探针有效:page这种真的走registerItem(..., packageId)的类型两个键都在、true治愈成功。#5927(deleteMetaItem 回执,同文件)—— 否,未触其面。 本 PR 只碰
applyObjectRegistryMutation/applyRegistryWriteThrough的调用点与rollbackMetaItem;deleteMetaItem及其回执构造、restoreArtifactRegistryView一行未动。#5488 / #5311(
saveMetaItem的 api 命名空间门,下轮)—— 定价不变,面不相交。 那两单要动的是saveMetaItem前半段的类型门/命名空间判定(environmentId !== undefined那一块 gate),本 PR 的 packageId 管线全部落在持久化之后的 registry 写透(if (mode === 'publish')之后)以及rollbackMetaItem,两者之间隔着repo.put。唯一的间接影响是正向的:写路径之后isArtifactBacked('object', …)对租户自建对象稳定为 false(靠强制盖章,而不是靠哨兵字符串),那两单要读这个判据时拿到的答案比现在更可靠。PR2 的入场券(PR1 合入后 boot 半边还剩什么,精确到行为差异):
loadMetaFromDbobject 分支那一行(现protocol.ts:10657一带):record.packageId || 'sys_metadata'→(record as { package_id?: string | null }).package_id || 'sys_metadata'。该分支已经在盖_provenance: 'org',所以 PR2 不需要再处理盖章。app.myapp上的对象,本次会话内创建/编辑后归属键是app.myapp(包过滤看得见),重启之后归属键退回'sys_metadata'(包过滤又看不见了)。即分类 bug 仍在,但只在重启后出现,且不再有静默丢编辑的风险 —— 因为两侧都盖'org',isArtifactBacked在两种键下都是 false,cloud#970 不会因为键不一致而复活。这正是裁决所说「先修写路径,之后任意时点落 boot 都可发布」。package_id的 object 行 → 断言 registry 归属是真实 id、_provenance是'org'、且紧接着的一次saveMetaItem成功(重启后可编辑,cloud#970 的重启面)。objectql/src/registry.ts那段契约注释:去掉本 PR 加的「描述的是契约,还不是代码」标注。package_id IS NULL#6215 无依赖关系。Generated by Claude Code