Skip to content

fix(metadata-protocol): allowRuntimeCreate:false 在每一种 kernel 上都生效 —— PUT /meta 不再创建注册表声明为 code-only 的 job / agent (#5086) - #5263

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-5086-allow-runtime-create-enforced
Aug 4, 2026
Merged

fix(metadata-protocol): allowRuntimeCreate:false 在每一种 kernel 上都生效 —— PUT /meta 不再创建注册表声明为 code-only 的 job / agent (#5086)#5263
os-zhuang merged 1 commit into
mainfrom
claude/issue-5086-allow-runtime-create-enforced

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Fixes #5086

结论先说:门是有的,只是挂在了一个错误的条件上

saveMetaItem 里 ADR-0005 的两级授权门(not_overridable / not_creatable)一直存在,也一直是对的 —— 但它整个包在 if (this.environmentId !== undefined) 里:

if (this.environmentId !== undefined) {
    // …not_overridable / not_creatable…
}

environmentId行作用域键(给 sys_metadataenvironment_id),不是授权信号。凡是没有它的 kernel,这道门整个不跑。而这并不是什么冷门拓扑:

  • objectstack.config.ts 里带了已实例化插件(showcase 就是)⇒ isHostConfig 为真 ⇒ shouldBootWithLibrary 为假 ⇒ createStandaloneStack 那条分支被跳过;
  • 于是 CLI 的轻量装配走 packages/cli/src/commands/serve.ts 的自动注册:new ObjectQLPlugin(),不带 environmentId;
  • 结果就是旗舰 showcase(以及所有同形状的自托管 app server)—— 它的 PUT /api/v1/meta/* 面向的是最终用户 —— 整个 ADR-0005 授权门是关着的。

issue 的复现正是在 "a real showcase boot" 上做的(#5145),所以拿到 200。

补一条实测出来的、比推理更有说服力的细节:这个洞恰好只对"没有写通道的类型"张开hook 这类 allowRuntimeCreate: true 的类型走 repository 写路径,SysMetadataRepository.assertAllowed() 在同一台无 environmentId 的 kernel 上照样 403(实测见下);而 job / agent 因为两个标志都是 false,useRepoPath 为假,落到了那条 legacy raw-engine 分支 —— 那条分支没有任何门。声明得最严的两个类型,恰恰是唯一能穿过去的两个。

改了什么

注册表条目同时声明 allowRuntimeCreate: falseallowOrgOverride: false 的类型,等于声明"没有任何运行时写通道"。这个判断只取决于类型声明,与部署拓扑无关,所以它被提到 environmentId 判断之外,在落库前、draft 与 publish 一视同仁地拒绝:

写入 之前 现在
PUT /meta/job/*(单 kernel 宿主) 200 success 403 NOT_CREATABLE
PUT /meta/agent/*(单 kernel 宿主) 200 success 403 NOT_CREATABLE
同上,但名字由代码包提供 200 success 403 NOT_OVERRIDABLE
project-scoped(cloud)kernel 403 403(不变)

拒绝语句点名类型、点名产生该判决的两个标志、给出该类型自己注册表条目里的 filePatterns 作为处方,并说明 OS_METADATA_WRITABLE 逃生舱 —— 处方从注册表读,新类型被打上标志的当天就有正确提示,这里没有第二份需要同步的表(Prime Directive #8)。

落点为什么是 packages/metadata-protocol/src/protocol.ts 而不是 packages/runtime/src/domains/meta.ts:两个 HTTP 面(rest-server.ts 与 runtime dispatcher)都只是转调 protocol.saveMetaItem,migrateStoredMetadata / duplicatePackage 也从内部重入同一个方法。门放在这里,才是"每个 surface 都必然经过"的那一处。

边界:哪些没有改,以及为什么

  • ADR-0005 的单 kernel 让步保留。 那句 "Single-kernel deployments (no projectId) keep their existing behaviour (any type writable)" 写的是 overlay 白名单(当时代码里只有 OVERLAY_ALLOWED_TYPES = {view, dashboard}),allowRuntimeCreate 那时还不存在。所以仍然可运行时创建的类型(object / hook / field / seed / mapping …)在单 kernel 上行为完全不变 —— 这也是本 PR 刻意不去动的部分:把 artifact-backed 的 not_overridable 一并放开,会让每个自托管部署的"改包内 object"当场 403,那是另一个量级的合同变更,不该搭车。
  • supportsOverlay 判不了,所以没有假装判。 spec 自己的 TSDoc 写得很清楚:supportsOverlay 描述的是 能力(loader 能否合并 overlay),allowOrgOverride 才是 许可,并且明说 "a write targets a type with this flag unset → 403 not_overridable"。而且 object / flow / action / seed / hook 都是 supportsOverlay: false 却按设计可运行时写 —— 把它当写入门会把这些类型全部拒掉。issue 里提到的"给 supportsOverlay: false 的行回一句 customization overlay"是文案问题,已另开 保存成功的回执一律自称 "customization overlay",包括注册表声明 supportsOverlay:false 的类型(object / flow / action / seed / hook) #5265
  • allowOrgOverride 的 org 维度按 ADR-0005 PR-10d.7 两级模型不动。 "无 artifact 的新建只需要 allowRuntimeCreate" 是被 ADR 记录且被测试钉住的决定,不在本 issue 范围内推翻。
  • deleteMetaItem 不同步收紧。 删除一条本不该存在的 code-only 行是修复,必须保持可行 —— 这次 bug 已经在真实环境里造出过这种行。实测确认删除侧没有对应的洞:非 code-only 类型走 repository,assertAllowed() 照常拦;code-only 类型的 legacy 分支只会删 sys_metadata 里的行,删不到 artifact。

验证

单测(新增 22 条,数据驱动:code-only 集合从 DEFAULT_METADATA_TYPE_REGISTRY 推导,新类型被打标志时缺 probe 会直接红)+ 真实 showcase boot 实测(pnpm dev -- --fresh -p 39517,issue 原样 payload):

PUT /api/v1/meta/job/rc3_runtime_job  → 403
{"error":"[not_creatable] Metadata type 'job' is code-only: … allowRuntimeCreate=false and
 allowOrgOverride=false … Declare it in source (**/*.job.ts) and redeploy. An operator may set
 OS_METADATA_WRITABLE=job …","code":"NOT_CREATABLE"}

PUT /api/v1/meta/agent/rc3_agent_probe → 403 NOT_CREATABLE
PUT /api/v1/meta/view/rc3_probe_view   → 200 (未被误伤)
GET /api/v1/meta/job                   → 只剩 artifact 的 showcase_health_sweep
PUT /api/v1/meta/hook/showcase_normalize_task_title (artifact-backed)
                                       → 403 NOT_OVERRIDABLE(repository 侧,本 PR 之前就有)

套件:

@objectstack/metadata-protocol  40 files /  370 tests passed
@objectstack/objectql          115 files / 1840 tests passed
@objectstack/rest               40 files /  608 tests passed
@objectstack/runtime            89 files / 1313 tests passed
pnpm check:type-check-coverage  OK (metadata-protocol 属 DEBT 台账,无 typecheck 脚本;DTS 构建通过)

回归性:把 protocol.ts stash 掉重跑新测试 → 10 failed | 12 passed,恢复后 22 passed

packages/objectql/src/overlay-precedence.test.ts 里那条 "single-kernel mode — gate bypassed" 的标本从 agent 换成了 hook:它钉的决定是"overlay 白名单在单 kernel 上不生效",agent 只是当时挑的最戏剧化的标本;换成 hook 后那个决定原样保留,并在旁边补了一条 code-only 仍被拒的断言。

顺带发现(已单独开 issue,未在本 PR 修)

packages/spec/** 零改动;未触碰 seed-loader.tsmetadata-manager.ts#5079 的枚举失效面。


🤖 Generated with Claude Code

https://claude.ai/code/session_01Pbu27iNUfQCHeuS551Rqo7

…PUT /meta no longer creates code-only job/agent items (#5086)

The ADR-0005 two-tier authorization gate sat behind `environmentId !==
undefined`. `environmentId` is a row-scoping key, not an authorization
signal: a host config with instantiated plugins skips the standalone
stack and boots `new ObjectQLPlugin()` with no environmentId, so the
showcase — and every self-hosted app server shaped like it — ran with
the gate disengaged. A `job` whose `handler` names no compiled function
persisted and answered 200.

A type declaring BOTH `allowRuntimeCreate: false` and
`allowOrgOverride: false` has no runtime write channel at all, in any
topology. That verdict is now taken from the registry declaration alone,
before persistence, for draft and publish saves alike, and returns the
catalogued NOT_CREATABLE / NOT_OVERRIDABLE 403 naming the type, the
flags, the source file pattern to declare it in and the
OS_METADATA_WRITABLE escape hatch.

The overlay whitelist keeps its single-kernel carve-out (ADR-0005
§"Whitelist enforcement" predates `allowRuntimeCreate` and speaks only
of that list), and `deleteMetaItem` stays permissive so a code-only row
that predates this refusal can still be removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pbu27iNUfQCHeuS551Rqo7
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 4, 2026 1:17pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/l labels Aug 4, 2026
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol.

3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata-protocol)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/metadata-protocol)
  • content/docs/releases/v9.mdx (via @objectstack/metadata-protocol)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

metadata: allowRuntimeCreate:false is not enforced — PUT /meta creates job and agent items the registry declares code-only

2 participants