Skip to content

fix(runtime): /ui discovery gates on the protocol service that serves it, not the vestigial ui slot (#4093) - #4146

Merged
os-zhuang merged 3 commits into
mainfrom
claude/dispatcher-handler-ready-1odd28
Jul 30, 2026
Merged

fix(runtime): /ui discovery gates on the protocol service that serves it, not the vestigial ui slot (#4093)#4146
os-zhuang merged 3 commits into
mainfrom
claude/dispatcher-handler-ready-1odd28

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

范围已收窄:本 PR 原本还带着 plugin-dev 的 data/ui stub 退役,但 ADR-0115(#4137 在本分支开着的时候把整张 stub 表连根退役了(Tier A+B+C 一次落地)—— 那一半已按 main 收敛。留下的是 #4137 刻意没动的 runtime 一侧缝隙(ADR-0115 的 Consumers 行明确写着 "runtime: no changes"),而且它在 ADR-0115 之后更重要了:现在任何模式下都没有东西注册 ui 槽位,routes.ui 永远不广告 —— 但 /ui 明明在服务。

改动

domains/ui.tsprotocol 服务应答 GET /ui/view/:object,没有它就 503;ui 槽位从不参与这个判断,而且全平台没有任何东西注册 ui 服务(plugin-dev 的 shapeless placeholder 是史上唯一占用者,已被 ADR-0115 退役)。按槽位占用判定 routes.ui 因此在两个方向都错:

  • 有 placeholder、没 protocol 的旧 dev 启动:广告一条只能 503 的路由;
  • 没 placeholder 的所有启动(生产从来如此,ADR-0115 后 dev 也如此):隐藏一条实际在服务的路由。

现在 routes.ui / services.uitypeof protocol?.getUiView === 'function' 判定 —— 与域 handler 自己的守卫逐字节一致,即 mcp 广告已经在用的那条规则(同一谓词 ⇒ 同一事实)。services.ui 报告真正服务它的实现(provider metadata-protocol,尊重其 __serviceInfo 自述),unavailable 提示语点名真实补救办法(注册 MetadataPlugin),而不是 svcUnavailable 的「install a ui plugin」—— 不存在这样的插件。

ADR-0076 的 #4093 批注同步收口:指向 ADR-0115 的整体退役,并记下本 PR 这条 discovery 缝隙;#4113(plugin-dev 之外仅存的 fabricator —— dispatcher /auth 域自带的 mock 登录)保持开放。

验证

  • runtime 936 / plugin-dev 10(main 的 ADR-0115 测试套)/ metadata-protocol 110 全过;build 71/71;eslint 干净
  • 新增 4 条 runtime 测试:有 protocol → 广告且真的服务(同一谓词、同一事实,getUiView 被以正确参数调用);无 protocol → 不广告、enabled: false、提示语点名 MetadataPlugin;回归钉ui 槽位被占而无 protocol → 仍不广告且 503(槽位买不来路由);wrong-shaped protocol(无 getUiView)→ 与域一致不广告
  • 合并 main(ADR-0115)后重新 pnpm install + pnpm build 再跑的全部结果(feat(plugin-dev)!: retire the stub table — DevPlugin assembles real plugins, empty slots stay empty (ADR-0115) #4137 给 plugin-dev 新增了 service-storage/service-realtime 依赖,旧 node_modules 会让 build 失败 —— 已在合并提交里处理)

关联

🤖 Generated with Claude Code

https://claude.ai/code/session_018UNGqBQcdJ2RYHtWgntJ9B

…ery gates on the protocol service (#4093)

The A-tier continuation of #4126, closing out the two items whose
prerequisites are now met (#4130 landed the data-slot discovery fix; this
change carries the ui-route prerequisite itself). `auth` stays untouched,
blocked on #4113.

data: in any boot with the objectql toggle on — the default — ObjectQLPlugin
registers the real engine and the stub never fired. In an engine-less boot it
was strictly harmful: both consumers of the slot carry a DELIBERATE empty-slot
degradation the stub silently replaced with fabrication. service-automation's
CRUD nodes document "no data engine → no-op success", but the stub's insert()
minted a `dev-<ts>` record id for data it discarded, which downstream flow
nodes then referenced as if stored; runtime's default-datasource plugin treats
an absent engine as "nothing to wire". The /data HTTP domain never reads the
slot (it resolves through callData), and discovery reads the occupant's
self-description since #4130 — an empty slot is answered honestly everywhere.

ui: the slot was pure fiction. Nothing in the platform registers or consumes a
`ui` service — the shapeless placeholder was its only occupant ever, and
domains/ui.ts serves /ui off the `protocol` service, 503ing without it. The
placeholder's only observable effect was advertising /ui in boots where the
route could only 503 (e.g. objectql disabled, or @objectstack/rest embedders
with enableUi off reading the builder's discovery).

Discovery now reads what /ui reads: routes.ui and services.ui gate on
`typeof protocol?.getUiView === 'function'` — handleUiRequest's own guard,
byte for byte, the same rule hasMcp already follows. That fixes both
directions: a placeholder-but-no-protocol boot no longer advertises a route
that can only 503, and a production boot (where the vestigial slot is always
empty but the protocol serves) no longer hides a route that works. The
unavailable message names the actual remedy — register MetadataPlugin — instead
of svcUnavailable's "install a ui plugin", which names a plugin that does not
exist.

With ui retired, the registration loop's shapeless-placeholder fallback is
dead code and is gone too: a slot this plugin has no implementation for is a
slot it must leave empty, not occupy with a nothing. A config-drift pin in the
tests asserts every core service name is either implemented or deliberately
listed in NO_DEV_STUB_SERVICES, so the placeholder cannot silently regrow.

Verified: plugin-dev 12, runtime 927 (4 new: route advertised + served with a
protocol; not advertised without one, with the remedy named; the old dev-boot
shape — ui occupant, no protocol — stays un-advertised AND 503s; wrong-shaped
protocol mirrors the domain), metadata-protocol 110, service-automation 457
pass; build 71/71; eslint clean. The two dispatcher↔builder parity tests
(metadata, data) pass against freshly built packages.

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

vercel Bot commented Jul 30, 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 Jul 30, 2026 12:48pm

Request Review

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/runtime.

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

  • content/docs/api/client-sdk.mdx (via packages/runtime)
  • content/docs/api/index.mdx (via @objectstack/runtime)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime)
  • content/docs/concepts/north-star.mdx (via packages/runtime)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/runtime)
  • content/docs/permissions/authentication.mdx (via @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/runtime)
  • content/docs/plugins/packages.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/runtime)
  • content/docs/releases/implementation-status.mdx (via @objectstack/runtime)

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.

… the deliberately empty slots

The drift check flagged plugins/packages.mdx for this change, and the #4126
sentence it carries was made stale by it — the never-stubbed list grew.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UNGqBQcdJ2RYHtWgntJ9B
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Jul 30, 2026
…superseded, runtime /ui discovery gate kept

#4137 retired the entire stub table wholesale (Tiers A+B+C), which includes the
data and ui retirements this branch carried incrementally — plugin-dev files
resolve to main's state. What this branch still adds is the runtime seam #4137
deliberately left untouched: discovery's routes.ui/services.ui gate on the
protocol service that actually serves /ui, which matters more post-ADR-0115
because no boot registers a ui-slot occupant at all now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UNGqBQcdJ2RYHtWgntJ9B
@os-zhuang os-zhuang changed the title fix(plugin-dev,runtime): retire the data and ui dev stubs; /ui discovery gates on the protocol service (#4093) fix(runtime): /ui discovery gates on the protocol service that serves it, not the vestigial ui slot (#4093) Jul 30, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review July 30, 2026 13:07
@os-zhuang
os-zhuang merged commit 43fc039 into main Jul 30, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/dispatcher-handler-ready-1odd28 branch July 30, 2026 13:07
os-zhuang added a commit that referenced this pull request Jul 30, 2026
…ble" names a package that exists (#4093 follow-up) (#4204)

Discovery tells a consumer two things about an absent capability: that it is
absent, and what to do about it. The first has been carefully honest since
#2462/#4000 — the whole D12 lineage is about it. The second was invented from
the slot name, and nothing ever checked it.

The dispatcher templated `Install a ${slot} plugin to enable` across twelve
slots. metadata-protocol hand-wrote a table where TEN OF FIFTEEN entries named
a package that does not exist: plugin-redis, plugin-bullmq, job-scheduler,
plugin-notifications, plugin-storage, plugin-automation, ui-plugin, plus
plugin-ai / plugin-search / plugin-workflow for slots nothing implements at
all. The same value is surfaced as discovery's `provider`.

That is the same `declared ≠ enforced` failure this lineage has been closing,
one level over: not "does the capability exist" but "is the fix real". A
package that cannot be installed is a dead end handed to someone at the exact
moment they are trying to fix their stack, and an agent reading discovery
cannot tell it apart from a package it should install.

CORE_SERVICE_PROVIDER + serviceUnavailableMessage() in @objectstack/spec/system
are now the one place that sentence is written, read by both builders — so the
two hosts cannot name different packages for the same slot, the drift #4089 and
#4130 closed for the `metadata` and `data` entries. A parity test asserts it.

Entries were verified against what actually calls `registerService` for each
slot, not against name similarity. That is how `notification` turned out to be
filled by @objectstack/service-messaging — the one slot whose package shares no
word with its name, and the proof that a name-derived guess cannot be right in
general.

Four slots (ai, search, workflow, graphql) have no implementation anywhere, so
`null` is a first-class entry and the message says nothing ships rather than
naming a plausible package. `ui` keeps the fuller sentence from #4146 — it is
served by the `protocol` service and nothing registers the `ui` slot, so
"install X" alone would read as "X fills this slot" and leave the reader
staring at an empty slot afterwards. Moving it into the shared table is what
the parity test surfaced: the two builders agreed on the package but not on the
sentence, and the richer one deserved to win rather than be flattened.

scripts/check-service-providers.mjs (wired into the lint workflow as
check:service-providers) fails CI on either way this table rots: a name that is
not a real workspace package, or a CoreServiceName slot with no entry at all.
Both modes verified by temporarily breaking the table. Same spirit as
check-route-envelope, which caught a stale exemption of mine earlier in this
lineage — the guard has to outlive the fix, or the next rename re-introduces it.

Also: spec's api-surface snapshot regenerated (0 breaking, 2 added — the guard
reported exactly the two new exports); the two docs that printed the old remedy
corrected (`protocol/kernel/http-protocol.mdx` showed `Install plugin-workflow`
/ `Install plugin-ai` for slots that have no implementation, and
`kernel/services-checklist.mdx` carried the old template down to its `a auth`
grammar bug); and objectql's discovery test follows `provider` to the scoped
name — a `provider`/`message` assertion can live in any package that exercises
a discovery builder, and that one was caught by Test Core rather than by
filtered local runs.

Verified: spec 7143, runtime 947, metadata-protocol 110, objectql 1238 pass;
build 71/71; pnpm lint and all eleven check:* scripts clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants