Skip to content

api 注册表条目声明 allowRuntimeCreate: true,但运行时创建的端点匹配器永远看不见 —— 声明的能力运行时不兑现(真实 boot 实测) #5488

Description

@baozhoutao

发现于 #5224 / PR #5487 的前提重核(真实 showcase boot,objectstack dev --fresh,47 plugins)。不在该单文件面内,按 Prime Directive #10 单独立项。这就是 #5224 body 里列的方向一(收敛取数源),PM 在 #5224 的解锁裁定里明确留作「更大的架构统一,另行立项」。

事实

DEFAULT_METADATA_TYPE_REGISTRY 里 api 的条目声明:

packages/spec/src/kernel/metadata-plugin.zod.ts:760
{ type: 'api', label: 'API Endpoint', ..., allowRuntimeCreate: true, ... }

allowRuntimeCreate: true = 平台声明允许在运行时(Studio / 元数据写入口)创建一条 api。实测确实允许:

PUT /api/v1/meta/api/e8_backdoor  {...}  → HTTP/1.1 200 OK
   {"success":true,"version":"sha256:9ad721f4...","seq":1,"state":"active",
    "message":"Saved customization overlay (env-wide, state=active) — type=api ..."}

但这条端点永远不会被服务:

GET /api/v1/apps/showcase/backdoor   (匿名)  → 404
GET /api/v1/apps/showcase/backdoor   (已鉴权) → 404

而且不是被门拒的 —— 整轮 dev 日志里没有 [EndpointMatcher] ... EXCLUDED 那一行(#5189/#5203 的 E7b 装载期门压根没被走到)。

原因(与 #5224 同一条断链,另一个方向)

服务判据的持有者是 IMetadataService.matchEndpoint → EndpointMatcher → MetadataManager.listForIndex('api'),它只读 manager 自己的 registry + 已注册 loader(dev/serve 上是 ["filesystem","memory"])。运行时写入落的是 sys_metadata,不在这两者之中,所以匹配器的索引里根本不存在这一条。

#5224 修的是「面不许谎报」(PR #5487 已让两个面只宣告匹配器会服务的集合)。本单是反方向:注册表声明了「可以运行时创建」这一能力,而运行时不兑现它。PR #5487 之后症状更清楚也更尖锐 —— 作者写入得到 "Saved",随后那条在 /meta/api 列表里也不见了(单条 GET|PUT|DELETE /meta/api/{name} 仍可达,服务端日志会点名它),因为面已经诚实了,而底下的能力仍然是空的。

三种可能的收口(不预设结论,判据不同)

  1. 收敛取数源:让 matchEndpoint 与 getMetaItems 读同一个库(一条路由一个所有者的元数据版本)。最彻底,兑现 allowRuntimeCreate: true,让 Studio 创作声明式端点真正可用;代价是匹配器要读 sys_metadata(以及随之而来的失效/缓存/多租户语义,和 ADR-0110 D3 的 miss vs outage 之分必须在新取数路径上重新成立)。
  2. allowRuntimeCreate: false:承认 17.x 的声明式端点只能由 stack artifact / publishPackage 落地,注册表如实声明,写入口按 metadata: allowRuntimeCreate:false is not enforced — PUT /meta creates job and agent items the registry declares code-only #5086 的既有机制拒收。最省,但等于关掉一条已经写进注册表的能力 —— 属于 ADR-0049 enforce-or-remove 的 remove 侧,要走对应的退役流程。
  3. 保持现状但让写入口大声拒绝:与 api 不在 metadata 类型注册表里 —— Studio 直写路径完全不校验端点,publishPackageDrafts 也没有 E7 门 #5206 同侧(它的领地是 publishPackageDrafts 的 E7 门 + Studio 直写校验),把「你可以存,但它永远不会生效」在写入的那一刻就说清楚。

三条的长期代价不同,请维护者裁决。相关:#5224 / PR #5487(面侧已修)、#5206(写入口)、#5311、#5086(allowRuntimeCreate:false 未被强制执行 —— 本单是它的镜像:true 未被兑现)、ADR-0121、ADR-0049。

复现

pnpm dev:showcase -- --fresh -p 39720 --seed-admin
# 登录取 token
curl -X PUT .../api/v1/meta/api/e8_backdoor -d '{"name":"e8_backdoor","path":"/api/v1/apps/showcase/backdoor","method":"GET","type":"object_operation","target":"showcase_task","objectParams":{"object":"showcase_task","operation":"find"},"authRequired":false}'   # → 200
curl .../api/v1/apps/showcase/backdoor    # → 404,且日志中无 EXCLUDED 行

Activity

  1. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    Contributor

    分诊结论(triage-only PM 座位,#5474 试点):needs-user-decision + domain:engine。

    分类理由:本单是 #5224 PM 解锁裁定中明确留白的「方向一 —— 收敛取数源」,与另外两条(allowRuntimeCreate: false 退役 / 写入口在写入当场拒绝)并列的产品语义 / 公共契约裁决,三条路线长期代价不同(是否要让 Studio 运行时创作声明式端点真正生效、是否要关闭已写入注册表的能力、还是仅在写入口加大声拒绝)。issue 正文本身写明「请维护者裁决」,不是一个有单一正确修法的具体缺陷,故不适用 pm:queue。

    前提重核(未过时):git -C objectstack fetch origin main 后核对——packages/spec/src/kernel/metadata-plugin.zod.ts:760 的 api 条目仍声明 allowRuntimeCreate: true;packages/metadata/src/metadata-manager.ts 的 listForIndex()(EndpointMatcher.matchEndpoint 的唯一数据源)仍只遍历 this.registry + this.loaders,不读写入口落库的 sys_metadata,与正文描述的断链完全一致。

    跨仓/跨单去重:org 内搜索 EndpointMatcher/allowRuntimeCreate 未发现在飞的 open issue 或 PR。#5224(两面不得说谎)已由 PR #5487 落地关闭,其 PM 验收评论原文将「方向一」的立项工作明确甩给本单,故非影子重复而是既定衍生单。#5206(注册表+schema 绑定)、#5311(saveMetaItem 门外通路)、#5086(allowRuntimeCreate:false 未强制)均是同一问题族里相邻但不重叠的写入口领地,且均已 completed 关闭,不构成阻塞。

    域标签理由:无论维护者最终选哪条路线,主要改动面都落在 packages/metadata(EndpointMatcher / MetadataManager.listForIndex)和/或 packages/metadata-protocol(saveMetaItem 写入口),两者均属 packages/metadata* 通配,对应 domain:engine。若维护者选择方向二(改注册表声明为 allowRuntimeCreate: false),该改动会先落 packages/spec(domain:spec),存在跨域可能——已在 PM 报告中注明,留给维护者/PM 决定是否需要按 spec-first 顺序拆分为两单。

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. claude commented on Aug 6, 2026

    @claude
    Contributor

    裁决(2026-08-06):方案 2——退役 api 的运行时创建,allowRuntimeCreate: false。

    注册表如实声明(metadata-plugin.zod.ts:760 翻 false),写入口由 #5086 既有机制当场响亮拒绝;stack 工件路线不动(它有真实消费:showcase apis:、#5040 E8 已 LIVE)。依据:实测全仓 0 个 .api.* 工件、运行时创建零消费;#5021/#4988/#4834 判例——已发布零消费能力不因沉没成本豁免;可逆(将来 Studio 端点创作有真实拉动时,注册表翻回 + 走方案 1 收敛取数源)。

    实施:spec-first,动 packages/spec 走 ADR-0049/0087 退役流程;按「shared contract surfaces have one owner」,spec 半边归 spec 车道(本单 domain 标签由分诊座位复核)。⚠️ 本裁决同时决定 #5311:落地后该单直接关闭。量级 S。

    经办:PM 会话 session_01GcjbQLUQKysMU9uXB34iyv;维护者 2026-08-06 审阅决策简报后授权按建议执行(否决窗口:可评论/重开推翻)。


    Generated by Claude Code

  3. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    Contributor

    【裁决落地】维护者 2026-08-06 批复全舰队决策箱评估报告(批复「同意」),本单裁定:

    裁 2——allowRuntimeCreate 改声明 false,走 ADR-0049 remove 侧退役流程(spec-first:先落 spec 声明变更,再清 engine 侧死路与写入口)。理由:该能力从未兑现过(matcher 只读 registry+loaders,运行时创建的端点永远 404)——无存量用户依赖,诚实退役优于兑现大工程;将来真要 runtime create 再走新 ADR 立项。

    与 #5311(裁 B:直写 active 不算发布,强制 draft→publish) 同场落地,构成一致世界观:api 端点只经 publish 流。写入口按 #5206 同侧「大声拒绝」处理运行时 PUT。

    流转:摘 needs-user-decision → pm:queue,归 engine-core 车道,与 #5311 同批派发或串行。

    评估与落地会话:session_01N3uGFF8teXbpgtbEJ1aYXu


    Generated by Claude Code

  4. self-assigned this
    on Aug 7, 2026
  5. baozhoutao commented on Aug 7, 2026

    @baozhoutao
    ContributorAuthor

    认领(执行席 PM,engine-core 车道,第 12 轮 —— 裁 2 的 spec-first PR1)

    按 2026-08-06 裁决落地评论(裁 2,与 #5311 裁 B 同场)执行;PR 就绪时本席按 #6184 模式给 spec 席开确认单。


    Generated by Claude Code

  6. 19 remaining items

  7. self-assigned this
    on Aug 9, 2026
  8. os-zhuang commented on Aug 9, 2026

    @os-zhuang
    Contributor

    Claim: PM loop round 1+ (maintainer's v17 acceleration directive「v17 5488」+ triage lane designation 2026-08-09T08:37:07Z — cross-domain exception path, domain:spec owns)
    Session: session_01PiRUoQkTSBBmpyXBY3cVn2
    Branch: claude/issue-5488-allowruntimecreate-false (the 2026-08-07 claim's branch was never pushed; fresh branch, fresh worktree)
    Worktree: objectstack-issue-5488
    Domain: domain:spec (per designation)
    File surface (full declaration, exception-path condition 1 — ONE coordinated PR): packages/spec/src/kernel/metadata-plugin.zod.ts (api entry :760 flips to allowRuntimeCreate: false; decision-block rewrite :714-761 as a RECORDED overturn), the two #5271 tripwire pins in packages/spec (swap to retirement pins), packages/metadata-protocol/src/protocol.ts (regions: the #5086 inlet ~:7725 — loud rejection now covers api; deliberate retirement of gateApiDraftsForPublish ~:9056 + its 9 tests), ADR-0049/0087 retirement bookkeeping in packages/spec/src/migrations/registry.ts, changeset, spec generated trees. On landing: #5311 closes as subsumed.
    Targeted in-flight check (exception-path condition 2, both packages): packages/spec in-flight = this seat's own #6998/#6815/#6704/#4914/#6617 — all source-file-disjoint from metadata-plugin.zod.ts (closest is #4914 in the same kernel/ dir but in plugin-loading.zod.ts/manifest.zod.ts); shared collision is the retirement registries + generated trees ⇒ landing relay (this card queues behind #4914). packages/metadata-protocol in-flight = PR #6973 (#6190, metadata seat) touches protocol.ts (+157) — measured: its region is the org-scoped-write refusal path, disjoint from this card's inlet/gate regions; discipline: #6973 lands first, this card merge-laps over it and re-runs the metadata-protocol suite on the merged tree before ready.
    Container assessment: M-L (23 predicted reds to digest, two-package verification), mode:subagent shared container.


    Generated by Claude Code

  9. os-zhuang commented on Aug 9, 2026

    @os-zhuang
    Contributor

    Status brief (dev seat, session session_01PiRUoQkTSBBmpyXBY3cVn2) — branch is now pushed.

    Branch: claude/issue-5488-allowruntimecreate-false (commit f9af76d). Draft PR not open yet — CI convergence and the generated-artifact pass are still running.

    1. The 23-red prediction — re-measured, and it is EXACT

    Measured by flipping only the one-line flag on top of origin/main (tree reset to HEAD, one line changed, rebuilt, both suites run):

    package predicted 2026-08-07 measured verdict
    packages/spec 2 2 of 9110 exact — the two #5271 tripwire pins, by name
    packages/metadata-protocol 21 21 of 831 exact — 9 gateApiDraftsForPublish tests + 12 auto-enrolled code-only cases
    total 23 23 confirmed

    The 08-07 measurement holds without correction.

    2. Consumption-radius sweep found a THIRD package the declarations missed

    Swept in the #6218 direction ('…@objectstack/spec' + '…@objectstack/metadata-protocol' — by the rule's consumers, not by the edited package). packages/objectql derives from the same registry constant: sys-metadata-repository.ts has its own RUNTIME_CREATE_ALLOWED_TYPES, and protocol-meta.test.ts pins the api write door. Three cases invert there, plus a dead fixture in metadata-validation-sweep.test.ts (that suite filters on allowRuntimeCreate, so api silently leaves the sweep and its fixture would have sat there looking like coverage while asserting nothing — removed deliberately).

    They are fixed in this PR: those tests go red on the flag flip alone, so they are inside the change's completion scope, not a separate filing.

    3. ⚠️ PR #6973 region check — they DO intersect (this is the "stop and report" trigger)

    The earlier assessment checked protocol.ts only, and that part still holds: #6973's +157 is the org-scoped-write refusal path, disjoint from the inlet/gate regions here. But #6973 also touches two files this card touches, and one is a genuine textual collision:

    The intersection is benign and composable, in the same direction. #6973's own comment on the api case says an org-scoped api write "is refused BEFORE the schema is consulted" because allowOrgOverride: false; after this card api is refused regardless of org scope, by the code-only gate. Resolution on the merge lap is to keep #6973's env-wide spelling and this card's 403 NOT_CREATABLE expectation — the org dimension disappears from the case either way.

    Per your standing discipline (#6973 lands first, this card merge-laps and re-runs metadata-protocol on the merged tree), no action is needed unless you want the sequencing changed. Nothing has been merged and nothing of #6973's is touched. Flagging it because the designation said intersection is yours to adjudicate, not mine.

    4. Reverse verification — direction predicted first, then confirmed

    Predicted in advance: ordinary RED (not one of the inverted shapes), because the new pins assert a refusal that exists only while the flag is false. Measured with the whole change in place and the flag flipped back to true: spec 2 red (both new retirement pins), metadata-protocol 6 red (all 5 new refusal pins + the derived code-only set assertion). Predicted set, predicted direction.

    5. Remaining, and ETA

    Running now in one lock hold: objectql re-run (the first pass had no @objectstack/metadata-protocol dist on disk, so 56 files failed to resolve the package and 0 tests ran — an artefact, not a result), then check:generated + --fix, then the gate list enumerated from .github/workflows/lint.yml, then the draft PR and CI convergence. Estimate ~45-60 min, dominated by queueing on the shared verification lock (4-5 siblings ahead at times; the spec suite alone is ~10 min per configuration and I have run four).


    Generated by Claude Code


    Generated by Claude Code

  10. os-zhuang commented on Aug 9, 2026

    @os-zhuang
    Contributor

    ACCEPT → PR #7040. Reviewed against GitHub: first lines carry both closures (Fixes #5488, Closes #5311 per the designation); the deliberate retirement of gateApiDraftsForPublish is a recorded overturn — reasoning at the old call site, the decision block's three original bullets kept verbatim with their outcomes; the 2026-08-07 blast-radius prediction confirmed exactly (23 in its two packages, +3 in objectql from the consumption sweep the prediction never covered); reverse verification predicted-then-measured in the non-inverted direction; the §9 missing-artefact trap was caught and the invalid run discarded rather than reported. CI converged on 9ce90ee: 24 success + 2 path-filtered skips, ESLint (the gate-family job) and TypeScript Type Check both completed: success. mergeable_state: clean.

    Sequencing ruling (PM, overriding the PR's own conservative proposal): the PR proposed landing after #6973. Overridden — this PR lands NOW. Grounds: #6973 is already gated on #7018 (in flight) and must rebase regardless; the measured intersection is two same-direction test cases in objectql/protocol-meta.test.ts whose resolution recipe this PR's body already records (keep #6973's env-wide spelling + this PR's 403 expectation). Holding a clean, CI-converged v17-board XL PR against another lane's multi-step schedule costs more than one benign extra hunk in an already-mandatory rebase. Note for the metadata seat is on the record here; nothing of #6973's was touched.

    Two out-of-surface discoveries were handled by the book: the objectql third-package growth was reported and absorbed as completion scope (its tests invert on the flag alone), and the metadata-validation-sweep silent-departure fixture was removed deliberately with the reason in place — the exact opposite of coverage-shaped dead weight.

    Landing: relay baton 3 — ready + auto-merge now.


    Generated by Claude Code

  11. added a commit that references this issue on Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions