Repository navigation
评估:plugin-dev「stub 填满槽位」这个早期设计是否整体作废 —— #4000/#4058 逐个退役到第 4 个之后,该问的是台面本身 #4093
Description
Activity
- added a commit that references this issue
on Jul 30, 2026 开完这条之后 #4082(step 1)落地了分类,PR #4086(step 2)落地了门。补三条据此更新的事实,都影响上面的分档:
1. 第 3 条证据(与 core fallback 重复)已被 #4082 部分消化
_fallback: true那个标记问题已在源头修好(本仓另开的 #4089 已关闭)。但B 档的实质没变:packages/core/src/fallbacks/memory-metadata.ts与 plugin-dev 的createMetadataStub仍然是两份并存的内存元数据注册表,现在还各自带一份内容几乎相同的__serviceInfo.message。registerInMemory那个"一个 bug 修两处"的收据依然成立,只是现在多了一处要保持同步的文案。2. A 档里
automation/notification/ai的 HTTP 面已经不再说谎 —— 所以删除是一个纯粹的"要不要",不再是"必须"PR #4086 的门让这三个(step 1 已标成
stub)的 HTTP 面等于空槽,端到端实测:services: automation stub handlerReady=false route=— notification stub handlerReady=false route=— ai stub handlerReady=false route=— requests: GET /automation handled=false GET /notifications handled=false POST /ai/chat 404 GET /ai/agents 200 {"agents":[]}留下的只是进程内的那一半 ——
execute()/send()仍会对进程内调用者报成功。这仍然是删除它们的理由(#4058 原文倾向的正是这条),但严重性比开这条 issue 时低了一档:不再有任何 HTTP 消费者能读到编造数据。3. 新发现:
data槽位的标签根本到不了 discoverypackages/runtime/src/http-dispatcher.ts:1010:data: svcAvailable(routes.data, 'kernel'), // 第三参 svc 省略
svcAvailable(route, provider, svc?)只在传了svc时才去读readServiceSelfInfo,而dataSvc从头到尾没被解析(getDiscoveryInfo的Promise.all里没有它)。所以data槽位被 dev stub 占住时,discovery 照样报status: 'available', handlerReady: true—— step 1 给它打的stub标签被这个调用点丢在地上。metadata-protocol 那侧的 builder 也把data放在硬编的 kernel-provided 块里,同样不读标记。这正是 D12 根因段点名的那个 hardcode("
svcAvailablehardcodesavailable")在data上的残留 —— #3028 修了函数,没修这个不传参的调用点。顺带确认了
/data域根本不消费data服务槽位:它走callData(protocol-first + ObjectQL fallback,domains/data.ts:73),只用deps.success/deps.error/deps.isMultiTenantHost。所以:- 删掉
datadev stub 对 HTTP 面零影响(A 档因此更容易走); - 但在删掉之前,那条
data:讨论线本身该修 —— 否则任何占住data槽位的假实现都会被 discovery 报成真的。
同理确认
ui:/ui域读的是protocol服务(domains/ui.ts:37),不是ui槽位。所以删掉无形状的ui占位会误伤routes.ui—— 那条路由实际是能用的,却因为hasUi按ui槽位判定而消失。A 档删ui之前,得先把routes.ui改成按真正服务它的东西(protocol)判定。这条是"declared ≠ enforced"的反向:广告挂在一个不服务它的槽位上。
据此,A 档的执行顺序建议改成:
security.permissions/security.rls/security.fieldMasker—— 撞红线的那三个,先走(可与环境护栏一并);auth(真实身份路径上的死代码)、data(HTTP 面零影响)—— 但data要先修上面第 3 条的data:调用点,否则删了 stub、下一个占槽者又会被报成available;ui—— 要先把routes.ui的判据从ui槽位改到protocol,否则是回退。
Generated by Claude Code
- 删掉
A 档第一步已开 PR #4126:三个
security.*stub 退役 + plugin-dev 在NODE_ENV=production下拒绝初始化(逃逸阀OS_ALLOW_DEV_PLUGIN=1)。同时 A 档的
auth一项挂住了,原因是准备删它的时候发现了 #4113 —— dispatcher 的/auth域自带 mock 登录,任意邮箱+任意密码返回 200 + 伪造 session token,而且它在packages/runtime里(跟发行版走,不是 dev-only)。关键点:plugin-dev 的 auth stub 从来没被那个分支调用过 —— 域要求.handler,stub 实现的是handleRequest。所以在 #4113 修掉之前,删 auth stub 对/auth/*的诚实度毫无作用。据此,A 档剩余项的状态:
项 状态 前置 security.permissions/.rls/.fieldMaskerPR #4126 — 环境护栏 PR #4126(同一个 PR,同一处发现) — auth阻塞 #4113(先处理 dispatcher 的 mock 登录) data待做 先修 http-dispatcher.ts的data: svcAvailable(routes.data, 'kernel')(不传 svc → 任何占槽者被报available)ui待做 先把 routes.ui的判据从ui槽位改到真正服务/ui的protocolB 档(与 core fallback 的
metadata重复)和 C 档(真实内存实现该归到各服务的 InMemory 策略)都还没动。
Generated by Claude Code
A 档第二步已开 PR #4146:
data与ui退役(security.*×3 + 生产护栏已随 #4126 合入 main)。两个前置的处置:
data的前置(discovery 那行 hardcode)已由 #4130/#4141 在 main 上修掉,直接受益;ui的前置(routes.ui改判据)由 #4146 自带 —— discovery 现在按protocol.getUiView(即/ui域自己的守卫)广告,placeholder 删除因此不会误伤实际能用的路由,反而修正了两个方向的既有失真(无 protocol 的 dev 启动过去广告一条只能 503 的路由;生产启动过去隐藏一条能用的路由)。A 档状态更新:
项 状态 security.permissions/.rls/.fieldMasker✅ 已合入(#4126) 环境护栏( NODE_ENV=production拒绝启动,OS_ALLOW_DEV_PLUGIN=1逃逸)✅ 已合入(#4126) dataPR #4146 uiPR #4146 auth⛔ 仍阻塞在 #4113 —— dispatcher /auth域自带的 mock 登录才是空槽时的真正应答者,不先退役它,删 auth stub 无效果#4146 合并后,A 档只剩
auth一项,且它的实质工作在 #4113。B 档 / C 档未动。
Generated by Claude Code
A 档完成。 最终落点与本 issue 开出时的逐槽位计划不同 —— ADR-0115(#4137)把问题整体收掉了:stub`` 表作为设计退役,plugin-dev 只做装配、不注册任何自有实现,空槽在 dev 与生产含义一致。
按合入顺序的完整收口:
落地 内容 #4126 security.*×3 退役(D12 红线)+NODE_ENV=production拒绝启动(OS_ALLOW_DEV_PLUGIN=1逃逸)—— ADR-0115 收尾更新将其选择定为 canonical#4137 整张 stub 表退役(Tier A+B+C 一次落地,含 data/ui/auth及全部 in-memory 实现;Tier C 以真实 service-storage / service-realtime 装配替代)#4146 ADR-0115 刻意留下的 runtime 缝隙: routes.ui/services.ui改按真正服务/ui的protocol.getUiView判定(域自己的守卫,hasMcp规则)—— stub 表退役后ui槽位在任何模式都为空,旧判据会永久隐藏一条在服务的路由前置调查中发现的 discovery hardcode(
data槽位)由 #4130/#4141 修掉;#4089 由 #4082 在源头解决。本 issue 范围内仅存的 fabricator 在 plugin-dev 之外:#4113 —— dispatcher
/auth域自带的 mock 登录(任意邮箱+任意密码 → 200 + 伪造 24h session token,在packages/runtime里随发行版走)。ADR-0115 的收尾更新点名它是"那类假实现仅存的地方"。B/C 档的遗留问题已由 #4137 的 Tier B/C 处置覆盖。建议:本 issue 可以关闭(或改题收窄为跟踪 #4113 的收口)。
Generated by Claude Code
- added 4 commits that reference this issue
on Jul 30, 2026
按 Prime Directive #10 记录。#4000 退役 1 个 dev stub,#4058 / PR #4086 又退役 3 个并把剩下的分了类 —— 两轮都是"逐个判断"。这条 issue 问上一层的问题:「任何未被真实插件占用的核心服务槽位,都注册一个 dev stub 填满」这个设计本身是否还应该存在。
不是要求立刻删,是要求评估。下面是评估所需的证据,以及一个分档建议。
现状(#4086 之后)
degraded(真干活,内存内)cache,queue,job,i18ncreateMemory*metadatafile-storage,search,realtime,workflowstub(编造)data,auth,security.permissions,security.rls,security.fieldMaskerui{}analytics(#4000),automation,notification,ai(#4058)证据
1. 原设计理由已被反证
代码里的理由是"填满完整的 kernel service map,让下游拿到正确的返回类型(数组、布尔、对象,而不是
undefined)"。但生产环境本来就是空槽 —— 所有这些服务都是 optional,没装插件就没有。所以每个下游本来就必须处理缺失,否则生产早就崩了。#4086 是直接反证:一次清空 4 个槽位,runtime 923 / plugin-dev 8 / objectql 1183 / metadata-protocol 99 全绿,端到端起 kernel + dispatcher 正常服务。没有任何"拿到 undefined 就崩"的下游冒出来。
这个理由成立的前提,恰恰是它想避免的那个 bug —— 如果真有下游不处理空槽,那个下游在生产里就是错的,应该修它,而不是在 dev 里喂假数据把它盖住。
2. 安全形状的假实现,撞在 D12/#3891 自己定的红线上
ADR-0076 D12 从 #3891 学到的结论写得很明确:「A fallback may degrade features, never security semantics」。plugin-dev 的这三个 stub 做的正好是后者:
而
packages/spec/src/contracts/security-service.ts:20明确说这三个句柄"are implementation internals and deliberately NOT part of this contract",由 plugin-security 注册 —— plugin-dev 却给别人的实现内部造了假,且方向与同一份契约要求的 fail-closed 相反("Access-narrowing answers fail CLOSED … A consumer must never treat a thrown error or a deny filter as 'no restriction'")。触发条件是现实的:plugin-dev 对每个子插件都是 try/catch 优雅降级,所以只要
@objectstack/plugin-security没装上(一条warn日志),allow-all + 无 RLS + 不脱敏就静默上线。这一档与"该不该保留 stub 台面"无关,本身就该走。补充一个精确性:
authstub 在真实身份路径上是死代码 ——resolve-execution-context.ts:104/120走的是authService.api.getSession()/getApi()/verifyMcpAccessToken(),而 stub 只实现了verify()/getCurrentUser()/handleRequest(),所以实际降级为匿名(fail-closed)。它不是活的越权洞。但verify() → { success: true, user: { roles: ['admin'] } }正是下一个消费者会直接信的形状,而且它占着槽位让 discovery 声称 auth 在场、让/api/v1/auth被广告出去。潜在,而非当下。3. 与
packages/core/src/fallbacks/重复,且有"同一个 bug 修两处"的收据cache/queue/job/i18n是委托 core 的createMemory*(这没问题)。但metadata是 plugin-dev 手写的第二份内存注册表,与packages/core/src/fallbacks/memory-metadata.ts(63 行)并存。代价已经付过一次,packages/core/CHANGELOG.md:1386原文:一个
registerInMemory缺失的 bug,要在两个地方修 —— 与 #3891 记录的"同一个安全门被造了两遍"完全同形,只是这次代价小。4. 这个包已发布、无护栏、仓库内无人使用、文档描述还是错的
@objectstack/plugin-dev不是 private(17.0.0-rc.0),会发到 npm。NODE_ENV/ 环境护栏:dev-plugin.ts里没有一处检查运行环境,装上就注册整套假实现(包括第 2 条那三个)。os dev走的是serve的真实 capability 装配(commands/dev.ts不引用 DevPlugin),examples / create-objectstack 模板 / apps 全无引用;只剩 CLI 一处测试注释和文档。content/docs/plugins/packages.mdx:347-350说它是"Metadata validation, schema introspection, debugging tools"。实际它是"一键装配 objectql + driver-memory + auth + security + hono + rest + dispatcher"外加 stub 台面 —— 两件事都不是文档写的那件。建议:分三档,不一刀切
A. 造假类 → 直接删(
data、auth、security.permissions、security.rls、security.fieldMasker、无形状ui)空槽就是生产语义,#4000/#4058 已经把这条路走通四次。第 2 条的三个安全槽位是这一档里最该先走的。
B. core 已有 fallback 的 → 删掉 plugin-dev 那一份(
metadata,以及把cache/queue/job/i18n的包装收干净)一份实现,一处修。与 #4089 合并做更省:那条要给 core 的五个 fallback 打
__serviceInfo,正好一起把"谁是唯一那份"定下来。C. 只有 plugin-dev 有的真实内存实现 → 唯一值得讨论保留的一档(
file-storage、search、realtime、workflow)它们确实有 dev 价值(不装服务也能试功能)。但正确归宿是真实服务自己的 InMemory 策略(
@objectstack/service-analytics已是先例 —— #4000 的迁移路径就是"装真引擎,它有 InMemory 策略")或 core fallback,而不是一份平行的假台面。所以这一档的问题不是"删不删",而是"退役前要不要先把对应服务的 InMemory 策略补上"。剩下的 plugin-dev = 纯装配插件。那才是它真正的价值,也应该顺手把文档描述改对。
待定问题
stub标记 + 启动 warn)?NODE_ENV !== 'production'断言(逃逸阀按 PD [WIP] Create a new release version #9 用OS_ALLOW_*)。考虑到第 2 条的安全语义问题,这条可能不该等这个评估 —— 也许应该独立成 issue 先走。关联:#4058、#4000、#4089、#4087、#3891、#3989、ADR-0076 D12。