fix(runtime): /ui discovery gates on the protocol service that serves it, not the vestigial ui slot (#4093) - #4146
Merged
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 18 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
… 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
…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
marked this pull request as ready for review
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
改动
domains/ui.ts用protocol服务应答GET /ui/view/:object,没有它就 503;ui槽位从不参与这个判断,而且全平台没有任何东西注册ui服务(plugin-dev 的 shapeless placeholder 是史上唯一占用者,已被 ADR-0115 退役)。按槽位占用判定routes.ui因此在两个方向都错:现在
routes.ui/services.ui按typeof protocol?.getUiView === 'function'判定 —— 与域 handler 自己的守卫逐字节一致,即mcp广告已经在用的那条规则(同一谓词 ⇒ 同一事实)。services.ui报告真正服务它的实现(providermetadata-protocol,尊重其__serviceInfo自述),unavailable 提示语点名真实补救办法(注册 MetadataPlugin),而不是svcUnavailable的「install a ui plugin」—— 不存在这样的插件。ADR-0076 的 #4093 批注同步收口:指向 ADR-0115 的整体退役,并记下本 PR 这条 discovery 缝隙;#4113(plugin-dev 之外仅存的 fabricator —— dispatcher
/auth域自带的 mock 登录)保持开放。验证
getUiView被以正确参数调用);无 protocol → 不广告、enabled: false、提示语点名 MetadataPlugin;回归钉:ui槽位被占而无 protocol → 仍不广告且 503(槽位买不来路由);wrong-shaped protocol(无getUiView)→ 与域一致不广告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 失败 —— 已在合并提交里处理)关联
security.*+ 生产护栏,已合并)、fix(runtime,metadata-protocol): every dispatcher domain gates on handlerReady, not slot occupancy (#4058 step 2) #4086(dispatcher 其余服务域仍只判槽位占用、不读 handlerReady —— #4000 在 analytics 一域落地后剩下的类推面 #4058 step 2 门,已合并)/authmock 是 stub 类假实现最后的幸存者,在packages/runtime里跟发行版走。ADR-0115 的收尾更新点名它是"那类假实现仅存的地方"。🤖 Generated with Claude Code
https://claude.ai/code/session_018UNGqBQcdJ2RYHtWgntJ9B