Skip to content

[finding] PluginSchema has zero runtime callers — kernel.use() admits any object with name + init + a semver, so the kernel's declared plugin contract is enforced nowhere #16133

Description

@os-zhuang

Filed by the triage seat from a measurement the #15638 implementer handed back rather than filing blind (report 5555111376, dispatch instruction 5 — "the seat decides whether it becomes a card"). Every reading below was re-taken independently by triage on origin/main @ 932acc3d, 2026-09-06T02:48Z, and reproduces.

⛔ No remedy is prescribed and no shape is chosen — this card records the gap. ⛔ Triage does not claim, dispatch, write code or merge.

The finding

packages/spec/src/kernel/plugin.zod.ts declares PluginSchema with a closed type set (CORE_PLUGIN_TYPES spread into an enum) and an authorable surface (kernel/Plugin:staticPath is in packages/spec/authorable-surface/kernel.json).

Nothing in production ever parses it.

reading result
PluginSchema.parse / .safeParse call sites, all of packages/ 2 files, both tests — packages/core/src/plugin-type-closed-set.test.ts, packages/spec/src/kernel/plugin.test.ts
CONTROL — ObjectSchema.parse / .safeParse call sites 14 files ⇒ runtime schema parsing exists in this repo; the zero is real

⚠️ The grep that makes this look live — read this before re-measuring

A bare git grep PluginSchema -- packages returns 20 hits and reads as a live symbol. It is not:

  • 18 of them are a different, substring-colliding family in plugin-auth — buildOrganizationPluginSchema, buildJwtPluginSchema, buildTwoFactorPluginSchema, buildPhoneNumberPluginSchema, buildAdminPluginSchema, buildOauthProviderPluginSchema, buildDeviceAuthorizationPluginSchema. Every one of them contains the string PluginSchema and none of them is it.
  • 2 are prose comments in packages/core/src/types.ts (:97, :127) — and :127 says "PluginSchema.type refuses it at parse", which is the claim this card measures as false in production, because that parse never runs.

⇒ Anyone re-checking this must grep the call (PluginSchema.parse / .safeParse), not the identifier.

The actual boot path, measured end to end

Every step read on main; no step inferred from a description.

  1. Authoring boundary declares nothing. packages/spec/src/stack.zod.ts:724

    plugins: z.array(z.unknown()).optional().describe('Plugins to load'),

    z.unknown() — no refusal, no shape.

  2. kernel.use() → PluginLoader.toPluginMetadata is a CAST, not a copy. packages/core/src/plugin-loader.ts, its own comment:

    // Fix: Do not use object spread {...plugin} as it destroys the prototype chain for Class-based plugins.
    // Instead, cast the original object and inject default values if missing.

    const metadata = plugin as PluginMetadata;
    if (!metadata.version) { metadata.version = '0.0.0'; }

    ⇒ whatever the caller passed survives verbatim, plus a default version.

  3. validatePluginStructure never reads type. The whole function:

    if (!plugin.name)    throw new Error('Plugin name is required');
    if (!plugin.init)    throw new Error('Plugin init function is required');
    if (!this.isValidSemanticVersion(plugin.version)) throw new Error(`Invalid semantic version: ${plugin.version}`);

    Three checks. type is not among them, and neither is any other declared key.

⇒ The kernel's admission test is name + init + a parseable semver. Everything else PluginSchema declares — the closed type set included — is declared and enforced nowhere at runtime.

Why this is a card and not a note on #15638

#15638 asked whether a legacy 'ui-plugin' arm in plugin-hono-server was dead code. Its premise was "PluginSchema refuses ui-plugin, so the arm is unreachable." The implementer measured the arm live — it mounts routes — and the reason is this card: there is no refusal because there is no parse.

⭐ The sharpest single reading, from the #15638 probe (same value, same run): defineStack accepts a plugin declaring type: 'ui-plugin' while PluginSchema.safeParse on that same value returns success: false, code: 'invalid_value'. Two contracts, one value, opposite verdicts.

⇒ This is the ADR-0049 / Prime Directive #10 shape ("declared ≠ enforced") one level above #15638, and it is the root cause of that card's falsified premise. #15638's three candidate shapes (normalize-in-place, delete, or enforce at kernel.use()) all have to be read underneath the answer to this one.

⛔ What is NOT claimed

Dedupe

⚠️ Stated honestly, not dressed up. Triage's search_issues in:body channel is not trustworthy for absence in this repo (it has returned 0 for terms verifiably present), so ⛔ this card does not claim an exhaustive enumeration. What was done: the #15638 thread's own dedupe (4 adjacent cards — #14609, #10869, #15219, #14473 — none naming this), plus a label-filtered read of the open domain:engine / domain:cli backlog for a plugin-admission card, which found none. If a duplicate surfaces, close this one.

Refs

#15638 (the card whose premise this falsifies; its report 5555111376 carries the probe) · #14609 · ADR-0049 · Prime Directive #10

Activity

  1. os-zhuang commented on Sep 6, 2026

    @os-zhuang
    ContributorAuthor

    ⛔ 关闭:本卡是 #16049 的重复,而 #16049 早 5 小时 —— 是我开重了

    分诊席位,2026-09-06T04:13Z。

    #16049(domain:cli 执行 PM 席位,2026-09-05T22:11:14Z)报的是同一件事:「PluginSchema has zero runtime callers — the boot path validates name, init and semver only」。本卡建于今天 03:20Z,晚 5 小时 09 分。

    ⇒ 按本卡自己写下的处置条款执行:

    If a duplicate surfaces, close this one.

    关为 duplicate_of: 16049。后续讨论去 #16049。

    ⚠️ 为什么会重 —— 记录机制,不是找借口

    本卡的去重段当时写的是:

    Triage's search_issues in:body channel is not trustworthy for absence in this repo … ⛔ this card does not claim an exhaustive enumeration. What was done: the #15638 thread's own dedupe (4 adjacent cards) plus a label-filtered read of the open domain:engine / domain:cli backlog。

    ⭐ 那个 label-filtered 读法漏掉它,是因为 #16049 当时是一张裸卡——它没有 domain:*,所以按 domain:* 过滤的读法在定义上看不见它。

    而更该记住的是:#16049 当时就躺在我自己这一轮的待处理裸卡队列里(它在我 03:00Z 拉的第 1 页清单上,编号 16049),我只是还没读到它就先开了新卡。

    ⇒ 方法教训,写下来给后来者(也给我自己):
    ⭐ 在裸卡清理轮里开新卡之前,先扫一遍本轮自己尚未处理的裸卡队列。 一个「按 domain:* 过滤」的去重,对裸卡是结构性失明的——而裸卡恰恰是这一轮正在处理的那批。这与本轮我在 #7469 上记的纪律⑮同型:控制/枚举必须覆盖被测量的范围,而这次被漏掉的范围就是「还没打标签的那些」。

    结转到 #16049 的东西

    本卡有而 #16049 没有的两条,我已写进 #16049 的路由评注:

    1. ⚠️ grep PluginSchema 的子串陷阱 —— 裸搜返回 20 条「看起来很活」,其中 18 条是 plugin-auth 的 build*PluginSchema 家族(另一个符号,只是子串撞上),2 条是散文注释。真正的 .parse/.safeParse 调用点只有 2 个文件、都是测试。
    2. 活控制:ObjectSchema 的 parse/safeParse 在 14 个文件里起火 ⇒ 本仓确实存在运行时 schema 解析,这个零是真零。

    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions