Part of #14122 · ADR-0130 Consequences 表第 6 行(Studio 按包分组)的服务端半边
Blocks: objectstack-ai/objectui#7177
业务需求
多包物(ADR-0130,#14240 装载路径 + #14354 安装闸已落地)把一个产品拆成 type: app + 若干 type: module 子包。Studio 的包选择器要正确显示每个子包能不能在里面写元数据——这是 Studio 决定给作者哪一套控件(可写 / 🔒 只读 + "duplicate into a writable base")的唯一依据。今天这个判定在客户端用 scope 反推,而服务端的权威规则根本不看 scope 的这一维,两边在多包物上必然分叉。
实测:两条规则不是同一条
服务端权威——isWritablePackage(engine, id)(packages/metadata-protocol/src/package-writability.ts:73,ADR-0070 D2;requireWritablePackage 与 SysMetadataRepository.assertAllowed 都引用它,#8146 维护者裁定"一个可写答案、徽标说真话"的落点):
if(e?.manifests?.has?.(packageId))returnfalse;// ① 启动时经 registerApp 注册的代码包 → 只读(不看 scope)constscope=e?.registry?.getPackage?.(packageId)?.manifest?.scope;if(typeofscope==='string'&&READ_ONLY_PACKAGE_SCOPES.includes(scope))returnfalse;// ② system / cloud → 只读returntrue;// ③ 其余(project 或缺省 scope 的 DB base)→ 可写
客户端启发式——objectui packages/app-shell/src/views/studio-design/packages-io.ts:38-42:
constscope=typeofm.scope==='string' ? m.scope : '';if(scope==='system'||scope==='cloud')continue;out.push({ id, name,writable: scope!=='project', namespace });分叉点在 ①:服务端判"代码包"的信号是 engine.manifests(ObjectQL.registerApp → engine.ts:4784this.manifests.set(id, manifest);#14240 的装载路径对物内每个子包都调 registerApp,所以每个子包都进这张表),客户端没有这张表,只能拿 scope 猜。
链条(每环已对 origin/main 核实):
| 环 | 事实 | 位置 |
|---|
| a | ManifestSchema.scope 默认 'project'只在 parse 时施加 | spec/src/kernel/manifest.zod.ts:263 |
| b | 装载路径刻意把原始 body 交给 registerApp(D7 逐位相同) | objectql/src/artifact-packages.ts 模块头 |
| c | installPackage 原样存 InstalledPackage.manifest | objectql/src/registry.ts |
| d | GET /packages 直接返回 registry.getAllPackages(),只有 ?status= / ?type= 过滤,没有可写判定 | runtime/src/domains/packages.ts:269-277 |
| e | 一个省略 scope 键的 module 子包(常态——作者依赖默认值)→ 客户端 scope='' → writable: true;服务端 ① → 只读 | — |
结果:Studio 给一个服务端必拒的包画上"可写"徽标,作者写到 WRITABLE_PACKAGE_REQUIRED 422 才知道。这正是 #8146 裁定要消灭的形状("两面回答同一个问题不一致,其中一面在说谎"),方向相反而已。
⛔ 客户端不能自己修。 直觉修法"缺省 scope 视为 project → 只读"会把 Studio 自建的 DB base 一并翻成只读:它们同样缺省 scope(服务端 ③ 判可写),客户端靠 scope === '' 才把它们显示为可写。区分二者的唯一信号 engine.manifests 只在服务端。所以修法落在平台。
方案
GET /packages 与 GET /packages/:id 的每个包记录附加一个字段:
writable: isWritablePackage(qlService,pkg.manifest?.id??pkg.id)
- 用同一个导出函数,不重拼规则(与
requireWritablePackage 同源,packages.ts:184 已 import)。 - 纯加法:在响应组装处 spread 一份带
writable 的副本,⛔ 不改写 registry 里的 InstalledPackage 对象,⛔ 不改 isWritablePackage 自身,⛔ 不动 READ_ONLY_PACKAGE_SCOPES。 engine 参数就是现有路由已解析的 qlService(与 requireWritablePackage 的传法一致)。
Pins(runtime 域测试,四态一个不少)
- 经
registerApp 注册、scope: 'project' 的代码包 → writable: false - 经
registerApp 注册、省略 scope 的代码包(模拟多包物 module 子包)→ writable: false ← 本卡的正题 scope: 'system' / 'cloud' → writable: false- 经
installPackage(不经 registerApp)、省略 scope 的 DB base → writable: true ← 保护 Studio 自建 base - 负向:
writable 之外,每行其余字节与今日响应逐位相同(不改 manifest、status、installedAt 等既有键) - 消融:把 ① 那一行的
manifests.has 改成恒 false,pin 2 必须红(证明 writable 真的读了服务端的表,不是又一份 scope 启发式)
边界
- ⛔ 不动
isWritablePackage 的语义;有争议走 ADR-0070。 - ⛔ 不动消费者市场面(ADR-0019 D2/D3)。
- ⛔ 不在本卡改 objectui;objectui#7177 消费这个字段(有则用,无则回落今日启发式以兼容旧服务端)。
- changeset:
@objectstack/runtime: patch(响应加法)。Clause-② 预期 NO:接受/拒绝面不动,只多一个只读字段;若 route ledger / 响应形状快照有门,按门的要求更新。
验收
Part of #14122 · ADR-0130 Consequences 表第 6 行(Studio 按包分组)的服务端半边
Blocks: objectstack-ai/objectui#7177
业务需求
多包物(ADR-0130,#14240 装载路径 + #14354 安装闸已落地)把一个产品拆成
type: app+ 若干type: module子包。Studio 的包选择器要正确显示每个子包能不能在里面写元数据——这是 Studio 决定给作者哪一套控件(可写 / 🔒 只读 + "duplicate into a writable base")的唯一依据。今天这个判定在客户端用scope反推,而服务端的权威规则根本不看scope的这一维,两边在多包物上必然分叉。实测:两条规则不是同一条
服务端权威——
isWritablePackage(engine, id)(packages/metadata-protocol/src/package-writability.ts:73,ADR-0070 D2;requireWritablePackage与SysMetadataRepository.assertAllowed都引用它,#8146 维护者裁定"一个可写答案、徽标说真话"的落点):客户端启发式——objectui
packages/app-shell/src/views/studio-design/packages-io.ts:38-42:分叉点在 ①:服务端判"代码包"的信号是
engine.manifests(ObjectQL.registerApp→engine.ts:4784this.manifests.set(id, manifest);#14240 的装载路径对物内每个子包都调registerApp,所以每个子包都进这张表),客户端没有这张表,只能拿scope猜。链条(每环已对
origin/main核实):ManifestSchema.scope默认'project'只在 parse 时施加spec/src/kernel/manifest.zod.ts:263registerApp(D7 逐位相同)objectql/src/artifact-packages.ts模块头installPackage原样存InstalledPackage.manifestobjectql/src/registry.tsGET /packages直接返回registry.getAllPackages(),只有?status=/?type=过滤,没有可写判定runtime/src/domains/packages.ts:269-277scope键的 module 子包(常态——作者依赖默认值)→ 客户端scope=''→writable: true;服务端 ① → 只读结果:Studio 给一个服务端必拒的包画上"可写"徽标,作者写到
WRITABLE_PACKAGE_REQUIRED422 才知道。这正是 #8146 裁定要消灭的形状("两面回答同一个问题不一致,其中一面在说谎"),方向相反而已。⛔ 客户端不能自己修。 直觉修法"缺省 scope 视为 project → 只读"会把 Studio 自建的 DB base 一并翻成只读:它们同样缺省
scope(服务端 ③ 判可写),客户端靠scope === ''才把它们显示为可写。区分二者的唯一信号engine.manifests只在服务端。所以修法落在平台。方案
GET /packages与GET /packages/:id的每个包记录附加一个字段:requireWritablePackage同源,packages.ts:184已 import)。writable的副本,⛔ 不改写 registry 里的InstalledPackage对象,⛔ 不改isWritablePackage自身,⛔ 不动READ_ONLY_PACKAGE_SCOPES。engine参数就是现有路由已解析的qlService(与requireWritablePackage的传法一致)。Pins(runtime 域测试,四态一个不少)
registerApp注册、scope: 'project'的代码包 →writable: falseregisterApp注册、省略scope的代码包(模拟多包物 module 子包)→writable: false← 本卡的正题scope: 'system'/'cloud'→writable: falseinstallPackage(不经registerApp)、省略scope的 DB base →writable: true← 保护 Studio 自建 basewritable之外,每行其余字节与今日响应逐位相同(不改manifest、status、installedAt等既有键)manifests.has改成恒 false,pin 2 必须红(证明writable真的读了服务端的表,不是又一份scope启发式)边界
isWritablePackage的语义;有争议走 ADR-0070。@objectstack/runtime: patch(响应加法)。Clause-② 预期 NO:接受/拒绝面不动,只多一个只读字段;若 route ledger / 响应形状快照有门,按门的要求更新。验收
check:dispatcher-error-vocabulary等 dispatch-gates 重推导后全绿。main起一个含app+ 缺省scope的module两包物,curl /api/v1/packages读回两行的writable均为false;同一服务上经 Studio 复制出的 DB base 读回true。截 curl 输出为证。