Skip to content

[runtime/metadata] 作者时规则只存在于 CLI:Studio/REST/MCP 的运行时授权面是第四扇门,26 条规则一条不跑——#4409 修完后最大的敞口 #4463

Description

@os-zhuang

现象

#4409/#4445 把 26 条作者时规则收进一张 registry,三个 CLI 命令(os validate / os build / os lint)由构造保证跑同一套门禁。但全仓只有一个包依赖 @objectstack/lint:

$ grep -rln '"@objectstack/lint"' --include=package.json packages/ apps/ core/
packages/lint/package.json
packages/cli/package.json

而元数据的运行时写入路径——Studio 编辑、REST /meta item CRUD(packages/runtime/src/domains/meta.ts)、MCP/AI agent 授权——最终都走 metadata-protocolsaveMetaItem,那里只做 per-type 的 Zod safeParse(packages/metadata-protocol/src/protocol.ts:5994,失败 422 带结构化 issues)。26 条规则一条不跑。

具体后果,用 #4409 的同一个实测例:租户在 Studio 里保存一个 expression approver 是坏 CEL 的审批流({ type: 'expression', value: 'record.owner ==' })。approver 的 value 在 Zod 里就是个 string,schema 全绿,存进 sys_metadata,经 registerFlow(service-automation/src/engine.ts:1551)注册,触发时节点在入口处失败——CLI 侧三个命令刚刚花了一个 PR 保证拦住的东西,换个门就直接进来了。

为什么这是 #4409 的下一层,不是新问题

#4409 的教训一句话:门禁的强度等于最弱那扇门。那个 PR 把 CLI 的三扇门对齐了——但三扇门都在 CLI 上。运行时授权面是第四扇门,而且:

  1. 它是租户唯一的门。 CLI 用户还有三个命令可跑;Studio 用户没有任何等价物。os lint 再完美也看不见租户 overlay(ADR-0033 的 sys_metadata 行根本不在 CLI 的 config 文件里)。
  2. 文档在做 declared ≠ enforced。 docs 把这套规则称为"the gate",AGENTS.md Prime Directive chore: version packages #10 的推论是 never advertise a capability the runtime doesn't deliver——而 gate 只存在于四个授权面之一。
  3. 有先例可循。 Prime Directive Add comprehensive test suite for Zod schema validation #12 的 worked example 正是这个形状:AI 授权的 create_record 用错 key,修法是授权技能修正 + publish-gate lint 在发布时拒绝(cloud#688),而不是运行时容错。cloud 侧已经承认"发布时跑 lint"是正确层次;framework 自己的发布面反而没有。

需要先定的四个产品决策(这是提 issue 而不是提 PR 的原因)

D1 — 门在哪:draft-save 还是 publish/activate?
saveMetaItem 对两种保存(state: 'draft' | 'active')已有统一的验证缝(protocol.ts ~1576-1588)。建议:gating 规则只挡 active(publish/activate),draft 保存永远放行、只回 advisory——草稿本来就允许是半成品,挡 draft 会毁掉 Studio 的编辑体验;而 active 正是"发布"的语义,和 os build 是同一个动词。

D2 — 输入形状不匹配:规则是 stack 级的,运行时写入是 item 级的。
26 条规则的签名都是 (stack) => findings,靠"stack 声明了什么"解析名字。运行时的等价宇宙是活的 registry(所有已装包 + 租户 overlay)。三个选项:

  • (a) 写入时从 registry 构造一个 stack 视图快照,跑全套——语义最对,但每次保存的成本需要测量;
  • (b) 给规则加 per-item 入口——API 大改,26 条一条条来,慢;
  • (c) 首期只按被写 item 的类型跑相关规则族(flow → approval/flow/expression 规则),用 (a) 的快照但按需构造。
    建议 (c) 起步、(a) 收尾。注意一个反直觉的红利:运行时宇宙比 CLI 的单包视图更全,validateCapabilityReferences 那类"可能由别的包提供"的 advisory 对冲在这里是可判定的——同一条规则在运行时门上反而能更严。

D3 — 严重性到 HTTP 的映射。
gating → 422,复用 Zod 失败已有的结构化 issues 信封(rule / path / message / hint 直接就是 AuthoringFinding 的形状);advisory → 2xx 但在响应里带 findings,Studio 渲染成警告。这样 objectui 不需要新协议,只需要读一个已有形状的新字段。

D4 — 逃生阀与迁移。
存量 sys_metadata 里一定有今天看是违规的行。门只挡新写入,存量走 ADR-0087 的老路(读路径不拒绝;applyConversionsToStoredItem 已经证明过这个不对称是对的)。需要一个 OS_ALLOW_* 形状的显式逃生阀吗?建议要——OS_ALLOW_UNLINTED_METADATA_WRITES=1,按 Prime Directive #9 的"故意难看"惯例命名,给迁移窗口用。

依赖结构上的一个障碍(需要在设计里解决,不是绕开)

metadata-protocol 不能简单加一个 @objectstack/lint 依赖了事:lint 包在 kernel boot path 之外是刻意的,它的重依赖(typescript ~9MB / sucrase)靠 lazy-deps.test.ts 钉住惰性。好在运行时门需要的规则族(flow/approval/expression/reference)恰好都不碰这两个依赖;react/jsx 页面源码类规则在运行时门上本来就可以先不接(Studio 的页面编辑另有 save-time 编译路径)。设计上应把"运行时门跑哪个子集"也写成 registry 数据——AUTHORING_RULES 每条已经有 tier/input/commands,加一个 surfaces: ['cli', 'runtime-publish'] 维度即可,理由照写。

防止历史重演的一条硬要求

无论落地成什么形状,运行时门必须消费 AUTHORING_RULES 同一张表,并且 #4445 的棘轮守卫要扩展到这个新 surface——否则我们就是在手工接线一个"第四命令",五次修过的漂移(#3583#3782#4384/#4394#4402#4409)会在一个新表面上原样长回来。这条不是建议,是这个 issue 存在的理由。

建议分批

相关

#4409 / #4445(CLI 三命令 registry,本 issue 的直接前提)、#4449(同型缺陷的另一方向:导出但零接线)、#4402#3583、ADR-0033(draft/active 语义)、ADR-0087(存量行的读路径不对称)、ADR-0049(enforce-or-remove)、cloud#688(publish-gate lint 先例,Prime Directive #12 worked example)。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions