Found while implementing #11637 (the REST server config seam). Filed, not fixed — #11637's declared surface is packages/rest/src/rest-server.ts, and this is a packages/core mechanism with its own gate family.
#11637 offered "declare configSchema on the REST plugin and let the kernel's plugin-config-validator do it" as one of its two candidate fixes. Measuring that candidate is what turned this up: the mechanism cannot run at all, for any plugin, so the candidate was structurally unavailable and #11637 landed the seam-side parse instead.
What was measured
On origin/main @ 7899f5745.
1. The one call site passes no config.packages/core/src/plugin-loader.ts:157-159:
if(metadata.configSchema){this.validatePluginConfig(metadata);// <- no second argument}and :406-419:
privatevalidatePluginConfig(plugin: PluginMetadata,config?: any): void{if(!plugin.configSchema)return;if(config===undefined){// In loadPlugin, we often don't have the config yet.// We skip validation here or valid against empty object if schema allows?// For now, let's keep the logging behavior but note it's delegatingthis.logger.debug(`Plugin ${plugin.name} has configuration schema (config validation postponed)`);return;}this.configValidator.validatePluginConfig(plugin,config);}config is always undefined here, so the early return always fires. "Postponed" to nothing: git grep validatePluginConfig finds no other caller outside PluginConfigValidator's own unit test.
2. There is nothing for it to postpone to.kernel.use(plugin) (packages/core/src/kernel.ts:198) hands pluginLoader.loadPlugin(plugin) the Plugin object only. A plugin factory captures its config in a closure — createRestApiPlugin(config) is the model — so no config value ever crosses into the kernel, and PluginMetadata carries no field for one. Even a correct validatePluginConfig(metadata, config) call would have nothing to pass.
3. No kernel plugin in the repo declares configSchema.git grep -n "configSchema:" packages -- ':!**/dist/**' ':!**/CHANGELOG.md' returns 20 hits and none is a kernel Plugin: they are automation node-executor JSON schemas (service-automation/src/builtin/*), the datasource driver catalog, plugin-approvals' approval-node descriptor, and spec/test declarations. So PluginConfigValidator — ~200 lines with its own unit test, its own formatZodErrors, validatePartialConfig and getDefaultConfig — has zero live consumers.
Why it matters
PluginMetadata.configSchema reads, at every author's call site, as the declared way to validate a plugin's configuration. It is documented as such (packages/core/ADVANCED_FEATURES.md:304, ADR-0025's "Config" section: "PluginConfigValidator validates plugin config against the plugin's schema"). A plugin author who declares one gets a debug line and no validation — the declared-not-enforced shape, one layer up from the one #11637 closes.
Not prejudged
Three shapes, and the choice is a real call:
Worth measuring before choosing: whether ADR-0025's manifest-driven load path ("configuration" in the plugin manifest) has a live call site that would have a config in hand, since that is the one caller for which the mechanism was designed.
Generated by Claude Code
Found while implementing #11637 (the REST server config seam). Filed, not fixed — #11637's declared surface is
packages/rest/src/rest-server.ts, and this is apackages/coremechanism with its own gate family.#11637 offered "declare
configSchemaon the REST plugin and let the kernel'splugin-config-validatordo it" as one of its two candidate fixes. Measuring that candidate is what turned this up: the mechanism cannot run at all, for any plugin, so the candidate was structurally unavailable and #11637 landed the seam-side parse instead.What was measured
On
origin/main@7899f5745.1. The one call site passes no config.
packages/core/src/plugin-loader.ts:157-159:and
:406-419:configis alwaysundefinedhere, so the early return always fires. "Postponed" to nothing:git grep validatePluginConfigfinds no other caller outsidePluginConfigValidator's own unit test.2. There is nothing for it to postpone to.
kernel.use(plugin)(packages/core/src/kernel.ts:198) handspluginLoader.loadPlugin(plugin)thePluginobject only. A plugin factory captures its config in a closure —createRestApiPlugin(config)is the model — so no config value ever crosses into the kernel, andPluginMetadatacarries no field for one. Even a correctvalidatePluginConfig(metadata, config)call would have nothing to pass.3. No kernel plugin in the repo declares
configSchema.git grep -n "configSchema:" packages -- ':!**/dist/**' ':!**/CHANGELOG.md'returns 20 hits and none is a kernelPlugin: they are automation node-executor JSON schemas (service-automation/src/builtin/*), the datasource driver catalog,plugin-approvals' approval-node descriptor, and spec/test declarations. SoPluginConfigValidator— ~200 lines with its own unit test, its ownformatZodErrors,validatePartialConfigandgetDefaultConfig— has zero live consumers.Why it matters
PluginMetadata.configSchemareads, at every author's call site, as the declared way to validate a plugin's configuration. It is documented as such (packages/core/ADVANCED_FEATURES.md:304, ADR-0025's "Config" section: "PluginConfigValidatorvalidates plugin config against the plugin's schema"). A plugin author who declares one gets adebugline and no validation — the declared-not-enforced shape, one layer up from the one #11637 closes.Not prejudged
Three shapes, and the choice is a real call:
Plugin/PluginMetadataa config field the factory populates, and pass it atloadPlugin. Makes the mechanism real for every plugin, and is the only shape that would let a plugin's schema cover its whole config rather than the slice a seam happens to reach.PluginConfigValidatorand theconfigSchemafield under ADR-0049 enforce-or-remove, with a tombstone, and let each plugin parse its own config at its own seam (which is whatRestApiConfigSchemaconstrainsapi.versionwith a regex the REST server never runs — the seam casts instead of parsing, soapi.version: ''is accepted and mounts the whole API at/api//#11637 does). A capability with zero consumers earning its keep on the strength of a docstring is the startup-scope case for removal.configwhere one exists, keeping the field for the declarative/manifest loading path ADR-0025 describes.Worth measuring before choosing: whether ADR-0025's manifest-driven load path (
"configuration"in the plugin manifest) has a live call site that would have a config in hand, since that is the one caller for which the mechanism was designed.Generated by Claude Code