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.
Observation-class finding, measured while tracing whether an app config can supply
AuditPluginoptions on theos servepath (#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
namehas two different meanings depending on which kernel is running.packages/core/src/lite-kernel.ts:36-45— refuses:packages/core/src/kernel.ts:182-195— accepts and overwrites, with no check and no log line distinguishing it from a first registration:ObjectKernelis the oneos serveruns (runtime.ts:56constructs it;serve.ts:1467takes it fromruntime.getKernel()), so the production path is the silent-overwrite one. Note the earlier instance has already been throughpluginLoader.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:readyhook errors fail boot onObjectKernel, get swallowed into one error log onLiteKernel) and #5282 (ObjectKerneldoes not inheritObjectKernelBase, 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
AuditPluginsupersede 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.Dispositions
⛔ 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.