Skip to content

core(finding): duplicate plugin registration throws on LiteKernel and silently overwrites on ObjectKernel — a fourth instance of the two-kernel semantic split #9864

Description

@os-steve

Observation-class finding, measured while tracing whether an app config can supply AuditPlugin options on the os serve path (#9540 / PR #9860). Filing because it is a new instance of an already-documented structural seam, not a new theory.

Measurement

Registering two plugins with the same name has two different meanings depending on which kernel is running.

packages/core/src/lite-kernel.ts:36-45 — refuses:

constpluginName=plugin.name;if(this.plugins.has(pluginName)){thrownewError(`[Kernel] Plugin '${pluginName}' already registered`);}this.plugins.set(pluginName,plugin);

packages/core/src/kernel.ts:182-195 — accepts and overwrites, with no check and no log line distinguishing it from a first registration:

constpluginMeta=result.plugin;this.plugins.set(pluginMeta.name,pluginMeta);this.logger.info(`Plugin registered: ${pluginMeta.name}@${pluginMeta.version}`,{});

ObjectKernel is the one os serve runs (runtime.ts:56 constructs it; serve.ts:1467 takes it from runtime.getKernel()), so the production path is the silent-overwrite one. Note the earlier instance has already been through pluginLoader.loadPlugin() by then — it is loaded, then dropped from the map, and only the later one bootstraps.

Why it is worth a card

This is the same shape as the closed #5170 (kernel:ready hook errors fail boot on ObjectKernel, get swallowed into one error log on LiteKernel) and #5282 (ObjectKernel does not inherit ObjectKernelBase, so hook dispatch semantics are written twice). Both were about one contract with two implementations that disagree; this is a third contract in the same pair, found the same way — by reading both rather than by anything mechanical.

The user-visible consequence today is benign-to-useful: it is what lets an app config's AuditPlugin supersede the CLI's auto-registered one (#9863). But it is undeclared, untested, and order-dependent, so it is load-bearing behaviour nobody has agreed to. The failure direction is the one that costs: a plugin silently replaced by a differently-configured instance of itself boots clean and logs "Plugin registered" twice, which reads as two plugins.

⚠️ Note #8357 (closed) recorded a related shape one layer up — serve's marketplace arm silently replacing a host config's own plugins — so the "silent replacement" failure has been paid for once already at a different altitude.

Dispositions

  • Make the two agree, in whichever direction the maintainer picks (refuse, or overwrite-with-a-warning), and pin it in a test that runs against both kernels.
  • Decide the two kernels are allowed to differ here and document it — but then the difference belongs in a declared contract, not in whichever file a reader happens to open.

⛔ Whichever way it goes, #9863 should be resolved first or together: it currently depends on the overwrite behaviour without saying so.

Refs: #9540 / PR #9860 (where it was measured) · #9863 (the dependent finding) · #5170 · #5282 · #8357.

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions