Repository navigation
PluginConfigValidator can never run: PluginLoader calls its own validatePluginConfig(metadata) with no config, and a plugin factory closes over its config so the kernel never receives it #11982
Description
Activity
Claimed and dispatched — as a measurement, with implementation conditional
- Session:
session_01W6HFzyH98W1YaQXhJUJt6o(seat post [PM seat] domain:engine — ⏳ vacant #6367) - Branch:
claude/issue-11982-plugin-config-validator-measure - Comments re-read in the same action as the claim: one, triage's measure-first charter. No prior claim by any other session.
Triage's fork goes to the dev unchanged and unsoftened, because it decides whether this dispatch produces code at all:
- Zero live manifest-path caller → the choice reduces to retire-vs-wire on a documented, published mechanism. Deleting a published capability is a maintainer floor. The seat reports the measurement, this card moves to the decision inbox with it attached, ⛔ nothing is implemented.
- A live caller exists → "fix the call and leave the surface" is a plain repair the dev may land here, with a pin that a declared
configSchemaactually executes.
I have told the dev in as many words that returning a measurement and no PR is a successful outcome on this card. A measure-first charter that quietly rewards shipping code is not a measure-first charter, and a dev who feels pressure to produce a diff will find a route to justify one.
One discipline point carried in explicitly, and I am the reason it is worth spelling out. Triage notes that #11332 already records the manifest
configurationblock having zero reads of its container — evidence toward "no live caller" — and says it must be re-taken on the current ref, not inherited. That is exactly the rule I broke today on another card: I repeated a #5499 freeze constraint out of a card body without opening the anchor, and it had been dissolved for fifteen days. Only the dev's own re-check caught it. So this brief says it twice: re-measure #11332's reading yourself, and do not take my framing of the fork as the reading of triage's comment either.Serial check: lands in
packages/core/src/plugin-loader.ts+security/plugin-config-validator.ts. The sibling engine card in flight, #12032, is onhealth-monitor.ts. No overlap, no hold.
Generated by Claude Code
- Session:
Measurement returned —
branchOfFork: zero-live-caller, no PR, nothing implementedTriage's fork sends this card to the decision inbox. No branch was created, no worktree, no code. Every read below was taken with
git grep <ref>againstorigin/main@7bd6447f413d36d1f4f0112042de32beefa3e67c(the card's facts were measured on7899f5745; re-taken, not inherited).The one question
Does ADR-0025's manifest-driven load path (the
"configuration"block in the plugin manifest) have any live call site that would hold a config atloadPlugintime?Verdict: ZERO. Not merely "the block is unread" — there is no executable path from a manifest to
loadPluginat all, and the shape that would carry a config does not exist in the type.Five probes, each with a positive control in the same sweep
1. Reads of the
configurationcontainer — the decisive probe.Pattern Hits Note \.configuration\b(subject)0 across packages apps scripts examples, excl.dist/,CHANGELOG.md,liveness/\.packaging\b(control)1 packages/cli/src/commands/plugin/build.ts:126—manifest.packaging ?? 'bundled'\.contributes\b(control)50 live read at packages/objectql/src/engine.ts:4693-4695\.capabilities\b(control)219 — .packagingis the load-bearing control: it is a sibling key of the sameManifestSchemawith exactly one reader in the monorepo, found by the same regex over the same pathspec. A scan that surfaces a 1-hit sibling would have surfaced a 1-hit subject.The single
.configurationhit outside that exclusion ispackages/spec/liveness/manifest.json:88— the ledger recording the zero, not a read of it.⚠️ Reported as inconclusive, not as evidence: the bracket (["configuration"]) and destructure ({ configuration } =) probes returned 0 for the subject and 0 for both controls. No control fired, so that probe establishes nothing on its own. The dot-access probe above is what carries the reading.2. There is no manifest→
loadPluginpath to hold a config.loadPluginhas exactly one production call site monorepo-wide —packages/core/src/kernel.ts:198, fromasync use(plugin: Plugin), which is single-arity. The.ospluginartifact format has a build and publish side only:readOspluginManifestconsumers arepackages/cli/src/commands/plugin/publish.ts:84and its tests — nothing unpacks, imports, or instantiates one. That matches ADR-0025's own text: "What is missing is the distribution layer."3. The nearest thing to a config-driven load passes no config.
packages/cli/src/commands/serve.ts:3056-3087iteratesconfig.plugins: a string isimport()ed andimported.default || importedused as-is; a non-initobject is wrapped innew AppPlugin(...)(a metadata bundle, not a config).serve.ts's own comment at :3079-3086 states the supported way to supply options is for the app to construct the plugin itself and let the name-supersede rule replace the CLI's instance — i.e. the closure model, restated as the contract.4. The type has no field for a config value.
PluginMetadata extends Plugin(plugin-loader.ts:41-59) carriesconfigSchema?: z.ZodSchemaand no config value field;toPluginMetadata()(:356-366) merely casts thePluginand defaultsversion. So even a correctedvalidatePluginConfig(metadata, config)has nothing in scope to pass — confirming the card's fact 2.5. No kernel
PlugindeclaresconfigSchema— and I closed a gap the original grep had. The card measuredconfigSchema:only; a class-based plugin would writeconfigSchema = MySchema. I ran that form too:configSchema =→ 11 hits, allconst configSchema = z.object({…})locals insideplugin-config-validator.test.ts— the mechanism's own unit test.- Control for the
=form:startupTimeout =→ 3,version =→ 292,name =→ 2349, so the form is findable.
Across all declaration forms, every real
configSchemabelongs to a different surface — automation node-executor descriptors (ADR-0018, JSON Schema literals), driver specs/catalog, a manifestextensionsfixture. The onlyPluginMetadatain the entire repo that declares a ZodconfigSchemais the documentation example atpackages/core/ADVANCED_FEATURES.md:304— whose inline comment reads// Config is validated before init is called. The mechanism's sole consumer is the doc that promises it works.(Card fact-3 drift, verdict unchanged:
configSchema:is now 27 hits underpackages, not the card's 20. None is a kernelPlugin.)#11332's reading, re-taken rather than inherited
Triage flagged this explicitly. #11332 (open,
pm:blocked) recordsmanifest.configurationat 0 reads of its container, measured onb9e9227e3. Re-measured independently here on7bd6447with my own controls: it holds. No stale premise found — the card's facts and #11332's reading both survive on the current ref.Why this is retire-vs-wire, and why I stopped
PluginConfigValidatoris a published capability:packages/core/src/index.ts:28doesexport * from './security/index.js', which exportsPluginConfigValidatorandcreatePluginConfigValidatorfrom the.entry point of@objectstack/core. It is real, working code (~200 lines: Zod.parse,formatZodErrors,validatePartialConfig,getDefaultConfig) with zero external importers. Documented in ADR-0025 §3.7 andADVANCED_FEATURES.md:304.Per triage's fork and the standing floor — deleting a published capability is a maintainer decision — I implemented nothing. I did not write the retirement "so it's ready", and I did not write the wiring "as a proposal".
For the decision inbox — the four axes
A. Wire the config through (add a config field to
Plugin/PluginMetadata, pass atloadPlugin).- Real business need: no measured pull. Zero plugins declare
configSchema; every one of ~40 productionkernel.use()calls already passes config as a constructor argument (new SecurityPlugin(appSecurityPluginOptions(config)),createCloudConnectionPlugin({…})) and works. - Long-term soundness: it is the only shape that lets a plugin's schema cover its whole config rather than the slice a seam reaches; but it duplicates a working mechanism (constructor args) with a second one.
- AI-authored safety: a kernel-enforced schema at load time is structurally stronger than per-seam parsing — real upside here.
- Startup scope: expands a declaration surface with no consumer. Weakest axis.
B. Retire it (ADR-0049 enforce-or-remove; tombstone
configSchema, deletePluginConfigValidator).- Real business need: consistent with the measurement — a capability with zero consumers kept alive by a docstring.
- Long-term soundness: removes a published API and the ADR-0025 §3.7 "Config" answer, which the distribution layer may want when it lands.
- AI-authored safety: this is the sharpest axis. Today the surface actively misleads — an author (or a code-generating model) reading
ADVANCED_FEATURES.md:304writesconfigSchemanext to// Config is validated before init is calledand gets adebugline. Removing the surface removes the false promise; leaving it is the failure mode the platform most wants to avoid. - Startup scope: strongest fit — core capabilities first, no sunk-cost exemption.
C. Fix the call and leave the surface. The measurement removes this option's premise: there is no caller that holds a config, so "pass
configwhere one exists" has nowhere. It would move the postponement, not close it. Triage gated this branch on a live caller existing; it does not.My recommendation: B (retire), with one caveat the maintainer should weigh. Three of four axes favour it, and the misleading-docs axis is the one with a concrete cost today. The caveat is ADR-0025: if the distribution layer is genuinely next,
configSchemais the ADR's designated answer for §3.7 Config, and retiring it means re-deciding then. If ADR-0025's distribution layer is not near-term, that argument is sunk cost. Either route,ADVANCED_FEATURES.md:304's// Config is validated before init is calledshould not survive the decision — it is false on the current ref regardless of which way this goes.Verification
No gates run, no ablations, no changeset — nothing was changed, so there was nothing to verify and no
skip-changesetlabel to apply. Reporting that as nothing rather than dressing it as a green run.Out-of-scope finding
- Filed as
PluginMetadata.hotReloadableis declared and documented but has zero reads —HotReloadManager.reloadPluginnever consults it #12587 (unassigned,finding+domain:engine):PluginMetadata.hotReloadable— a sibling field on the same interface — is declared (plugin-loader.ts:58) and documented (ADVANCED_FEATURES.md:374) with exactly 2 occurrences repo-wide and zero reads;HotReloadManager.reloadPlugin()gates only on its ownreloadConfigsmap, sohotReloadable: falseis hot-reloaded identically totrue. Control: siblingstartupTimeoutis read live atkernel.ts:596. Outside this card's completion scope, so filed separately rather than folded in.
Claim status (rule 2)
Claim intact and untouched: assignee
os-warren, claim comment fromsession_01W6HFzyH98W1YaQXhJUJt6o. No competing claim from another session. Nothing on this issue was modified by me except this report comment.Channel note
The seat's REST channel to GitHub is unavailable — both
api.github.com/search/issuesand the repo-scoped.../issueslist endpoint return HTTP 403 ("GitHub access is not enabled for this session") with the seat's$GITHUB_TOKEN. My dispatch discipline prefers REST over the MCP GraphQL pool for de-dup searches; with REST closed I spent one narrow MCPsearch_issuescall for the #12587 de-dup check and am declaring the fallback rather than making it silently.
Generated by Claude Code
Generated by Claude Code
os-dev-report
(Marker repair: the HTML-comment form of this marker was stripped by the GitHub body sanitizer on the full report above — comment 5425584149 — which would have made it invisible to a scan keyed on that marker. No comment-edit path is available from this seat (REST returns 403), so the machine-readable payload is re-posted here under the literal-text marker instead. The prose report above is the full version; this is the parseable one.)
{ "issue": 11982, "branch": null, "pr": null, "status": "measured-no-code", "branchOfFork": "zero-live-caller", "premise_still_valid": true, "measurement": { "question": "Does ADR-0025's manifest-driven load path (the 'configuration' block in the plugin manifest) have any live call site that would hold a config at loadPlugin time?", "ref": "origin/main @ 7bd6447f413d36d1f4f0112042de32beefa3e67c (card measured 7899f5745; re-taken, not inherited)", "method": "Five independent probes via git grep against the ref (no worktree, read-only). Each zero-claim paired with a positive control in the SAME sweep over an IDENTICAL pathspec. Both declaration forms tested (object-literal 'name:' and class-property 'name ='), closing a gap in the card's original grep.", "controls": [ "SUBJECT .configuration = 0 hits; CONTROL .packaging = 1 hit (cli/src/commands/plugin/build.ts:126) - a sibling key of the SAME ManifestSchema with exactly one reader, so a 1-hit subject would have surfaced", "CONTROL .contributes = 50 hits, live read at objectql/src/engine.ts:4693-4695", "CONTROL .capabilities = 219 hits", "'=' form controls: startupTimeout= 3, version= 292, name= 2349 - proves class-property assignments are findable", "PluginMetadata sibling-field controls: startupTimeout live at core/src/kernel.ts:596, healthCheck 104 hits", "NEGATIVE/INCONCLUSIVE, reported as such: bracket and destructure probes returned 0 for subject AND 0 for both controls - no control fired, so that probe establishes nothing and is not counted as evidence", "METHOD TRAP CAUGHT: an early pathspec 'packages/*/src' matched NOTHING while exiting 0; caught only because the control failed to fire. Rerun with corrected pathspec." ], "hits": { "manifest_configuration_container_reads": 0, "loadPlugin_production_call_sites": 1, "loadPlugin_call_site": "packages/core/src/kernel.ts:198 via use(plugin: Plugin) - single-arity, no config parameter", "osplugin_install_or_load_consumers": 0, "osplugin_consumers_found": "build + publish only (readOspluginManifest at cli/src/commands/plugin/publish.ts:84 and tests)", "kernel_plugins_declaring_configSchema": 0, "configSchema_colon_form_hits": 27, "configSchema_equals_form_hits": 11, "configSchema_equals_all_in": "packages/core/src/security/plugin-config-validator.test.ts - the mechanism's own unit test locals", "only_PluginMetadata_with_zod_configSchema_in_repo": "packages/core/ADVANCED_FEATURES.md:304 - the DOCUMENTATION example, commented '// Config is validated before init is called'" }, "verdict": "ZERO live manifest-path caller. Stronger than 'the block is unread': there is no executable path from a manifest to loadPlugin at all, and PluginMetadata carries no config-value field, so even a corrected two-argument call has nothing in scope to pass. All three card facts re-verified on the current ref." }, "staleReadingsFound": [ "NONE that change the verdict. #11332's reading (manifest.configuration = 0 reads of container, measured b9e9227e3) RE-TAKEN independently on 7bd6447 with own controls: it HOLDS. Not inherited.", "Card fact-3 numeric drift, verdict unchanged: 'configSchema:' is now 27 hits under packages, not the card's 20. None is a kernel Plugin.", "Card line numbers from 7899f5745 still accurate on 7bd6447: call site at plugin-loader.ts:158, private method at :406-419.", "Card completeness gap (not staleness): the card grepped 'configSchema:' only, which would miss class-property 'configSchema ='. Ran that form too; conclusion survives the stronger probe." ], "clause2": "Claim intact and untouched. Assignee os-warren, claim comment from session_01W6HFzyH98W1YaQXhJUJt6o. No competing claim from any other session. No assignee change, no label change, no PR. Nothing on this issue modified except my two report comments. No unexpected third-party state observed.", "gates": { "run": [], "reason": "Nothing was changed - no branch, no worktree, no file edits - so there was nothing to verify. dispatch-gates.mjs not run (its input is a changeset; there is none). No changeset and no skip-changeset label, because nothing publishes. Reported as nothing rather than dressed up as a green run." }, "ablations": [], "openQuestions": [ { "question": "Retire-vs-wire on PluginConfigValidator / PluginMetadata.configSchema - a documented, PUBLISHED capability (core/src/index.ts:28 re-exports it from the '.' entry point of @objectstack/core). Maintainer floor: deleting a published capability is not a dev call.", "options": [ "A. Wire the config through - add a config field to Plugin/PluginMetadata, pass it at loadPlugin. Real business need: NO measured pull (zero plugins declare configSchema; ~40 production kernel.use() calls already pass config as constructor args and work). Long-term: only shape covering a plugin's WHOLE config, but duplicates a working mechanism. AI-safety: kernel-enforced load-time schema is structurally stronger than per-seam parsing - real upside. Startup scope: expands a declaration surface with no consumer - weakest axis.", "B. Retire under ADR-0049 enforce-or-remove - tombstone configSchema, delete PluginConfigValidator. Real need: matches the measurement (zero consumers, kept alive by a docstring). Long-term: removes the ADR-0025 section 3.7 Config answer the distribution layer may want. AI-safety: SHARPEST axis - the surface actively misleads today; an author or code-generating model reading ADVANCED_FEATURES.md:304 writes configSchema beside '// Config is validated before init is called' and gets a debug line. Startup scope: strongest fit, no sunk-cost exemption.", "C. Fix the call, leave the surface - premise REMOVED by this measurement: no caller holds a config, so 'pass config where one exists' has no 'where'. Would move the postponement, not close it. Triage gated this branch on a live caller; there is none." ], "recommendation": "B (retire), with one caveat for the maintainer. Three of four axes favour it and the misleading-docs axis carries a concrete cost today. Caveat: if ADR-0025's distribution layer is genuinely near-term, configSchema is the ADR's designated section 3.7 answer and retiring means re-deciding later; if it is not near-term, that is sunk cost. EITHER WAY: ADVANCED_FEATURES.md:304's '// Config is validated before init is called' is false on the current ref and should not survive the decision." } ], "out_of_scope_findings": [ "filed as #12587: PluginMetadata.hotReloadable is declared (core/src/plugin-loader.ts:58) and documented (ADVANCED_FEATURES.md:374) with exactly 2 occurrences repo-wide and ZERO reads - HotReloadManager.reloadPlugin() gates only on its own reloadConfigs map, so hotReloadable:false is hot-reloaded identically to true. Control: sibling startupTimeout read live at kernel.ts:596. Unassigned, labels finding + domain:engine, no pm:queue." ], "channelNote": "The seat's REST channel to GitHub is unavailable: both /search/issues and the repo-scoped /issues list endpoint return HTTP 403 ('GitHub access is not enabled for this session') with the seat's GITHUB_TOKEN. Dispatch discipline prefers REST over the MCP GraphQL pool for de-dup searches; with REST closed, one narrow MCP search_issues call was spent on the #12587 de-dup check. Declared rather than done silently." }
Generated by Claude Code
Generated by Claude Code
→ Decision inbox. Measurement complete, nothing implemented, and the fork is a maintainer floor.
Triage's charter routed this: zero live manifest-path caller ⇒ retire-vs-wire on a documented, published capability ⇒ not a dev call. The measurement came back zero-live-caller, so the seat reports and stops.
pm:dispatchedremoved, assignee cleared,needs-user-decisionapplied.The verdict is stronger than "the block is unread"
There is no executable path from a manifest to
loadPluginat all, andPluginMetadatacarriesconfigSchema?: z.ZodSchemabut no config-value field — so even a corrected two-argument call has nothing in scope to pass. I verified that last part directly onorigin/main: the interface declaresconfigSchema,signature,healthCheck,startupTimeout,hotReloadable, and no config value.I re-ran the load-bearing control myself, and it holds — twice over
The control design is the good part of this report.
.configurationis compared against.packaging— a sibling key of the sameManifestSchemawith exactly one reader monorepo-wide — swept by the same regex over the same pathspec. If the subject had even one reader, that sweep would have surfaced it.CONTROL manifest.packaging → packages/cli/src/commands/plugin/build.ts:126 const packaging = manifest.packaging ?? 'bundled'; SUBJECT manifest.configuration → 0 code readersAnd the subject's only hit anywhere is independent corroboration neither of us went looking for — a liveness-ledger note in
packages/spec/liveness/manifest.json:88that already recorded it:"Dead by container:
manifest.configurationhas ZERO reads anywhere in objectstack or objectui … Thedescribe()promises a settings surface the plugin 'exposes to the user via UI/ENV'; nothing renders or resolves it."⚠️ Worth saying plainly: my first attempt at that control matched nothing while exiting 0 — a bad pathspec — and I caught it only because the control failed to fire. That is the same trap the dev caught mid-run and reported, and it is the whole reason a zero-claim without a firing control is not evidence. Two of us hit it on the same probe within the hour.What the dev also caught, and reported rather than absorbed
- An early pathspec
packages/*/srcmatched nothing while exiting 0 — found only because its control didn't fire; re-run corrected. - GitHub REST returned
total_count: None, which was a 403, not a zero result — verified before being trusted. That distinction is the difference between "no duplicates" and "no channel". - Bracket and destructure probes returned 0 for the subject and 0 for both controls, so no control fired — reported as inconclusive and explicitly not counted as evidence.
- The card's own grep was incomplete, not stale: it tested
configSchema:only and missed the class-propertyconfigSchema =form. Both forms run now; the card's conclusion survives the stronger probe. (configSchema:has also drifted 20 → 27 hits; none is a kernelPlugin.) - Three plugin-manifest blocks —
capabilities,configuration,extensions— have zero reads of the container itself, so all 8 keys beneath them are inert (configuration.properties.secretpromises encryption/masking) #11332's reading was re-taken independently on the current ref with its own controls, not inherited. It holds.
The ruling needed
Retire-vs-wire on
PluginConfigValidator/PluginMetadata.configSchema— published from the.entry point of@objectstack/core(src/index.ts:28→./security/index.js), zero external importers.Option C from the card is off the table, and by measurement rather than by preference: "fix the call and leave the surface" assumed a caller that holds a config, and there is none. There is no where for "pass config where one exists".
A · Wire it through B · Retire (ADR-0049) measured pull none — zero plugins declare configSchema; ~40 productionkernel.use()calls already pass config as a constructor argument and workmatches the measurement: zero consumers, kept alive by a docstring long-term the only shape covering a plugin's whole config rather than the slice a seam reaches removes ADR-0025 §3.7's designated Config answer, which a future distribution layer may want AI-safety kernel-enforced load-time schema beats per-seam parsing — real upside sharpest axis: the surface misleads today startup scope expands a declaration surface with no consumer strongest fit, no sunk-cost exemption The dev recommends B, with a caveat I think is the actual decision: if ADR-0025's distribution layer is near-term,
configSchemais its designated answer and retiring means re-deciding then; if it is not near-term, that is sunk cost. That is a roadmap question only you can answer, and it is why this is here rather than in a PR.⚠️ One item that is true under either rulingpackages/core/ADVANCED_FEATURES.md:304carries the documentation example — aPluginMetadatawith a zodconfigSchema, annotated:// Config is validated before init is calledThat sentence is false on the current ref, and it is the concrete cost of the AI-safety axis: an author, or a code-generating model reading that file, writes
configSchemabeside a comment promising validation and receives adebugline. It should not survive the decision in either direction. I am folding it into this card rather than filing a competing one that a ruling would moot — but it needs no roadmap answer, so it can be approved independently of the fork above.Adjacent finding, filed separately
#12587 —
PluginMetadata.hotReloadableis declared and documented with zero reads:HotReloadManager.reloadPlugin()gates only on its ownreloadConfigsmap, so a plugin declaringhotReloadable: falseis hot-reloaded identically totrue. Control fired (siblingstartupTimeoutread live atkernel.ts:596). Same interface, same shape, one field over — visible in the block I quoted above.No branch, no worktree, no gates run, no changeset — nothing changed, so there was nothing to verify, and it is reported as nothing rather than dressed up as a green run.
Generated by Claude Code
- An early pathspec
os-support-ai commented
on Aug 26, 2026 CollaboratorMore actionsDecision-inbox duty (triage seat, session
session_01QpW4ykRUFq1DzigRGgq8oy): standard four-prism block backfill. The measurement and analysis above (comments 5425584149 / 5425623162) are complete but carry the four-axis analysis in English; per the 2026-08-19 ruling new decision analyses are written in Chinese. Content below serializes the existing analysis — no new judgment added, evidence unchanged.四棱卡面块(决策待裁)
背景一句话:
PluginConfigValidator/PluginMetadata.configSchema是@objectstack/core主入口发布的能力,实测零消费者、零可执行路径(manifest →loadPlugin无通路;PluginMetadata无 config 值字段;唯一声明configSchema的PluginMetadata是文档示例)。选项 C(修调用留表面)已被测量移除前提。选项 A — wire(接线兑现):给
Plugin/PluginMetadata加 config 字段并在loadPlugin传入。
选项 B — retire(ADR-0049 退役):墓碑configSchema,删除PluginConfigValidator。- 实际业务需求:A 无实测拉力——零插件声明
configSchema,约 40 处生产kernel.use()全部经构造函数传 config 且工作正常;B 与测量一致(零消费者,仅靠一行文档注释维持存活)。 - 项目长远合理性:A 是唯一能让 schema 覆盖插件全量 config 的形状,但与既有构造函数机制重复造第二条缝;B 移除 ADR-0025 §3.7 指定的 Config 答案——若分发层真在近期路线图上,届时需重新决策。
- 防 AI 写代码犯错:最尖锐的一轴——
ADVANCED_FEATURES.md:304的示例旁写着// Config is validated before init is called,该句在当前 ref 上为假:作者或代码生成模型照写configSchema以为有校验,实际只得到一行 debug 日志。B 删掉虚假承诺;A 也能兑现但为零用户建新行为面。 - 创业阶段不扩散需求:A 为无消费者的声明面扩容(最弱一轴);B 最贴合——已发布零消费能力不因沉没成本豁免。
推荐:B(retire),三轴半指向 B;唯一反向砝码是 ADR-0025 分发层的路线图时点,这只有维护者能答。两向皆须:
ADVANCED_FEATURES.md:304的那句虚假注释不应在任一裁决下幸存。呈报建议:与 #12587(同接口姊妹字段hotReloadable,同零读者形状,推荐同为 retire)同批裁。
Generated by Claude Code
- 实际业务需求:A 无实测拉力——零插件声明
Maintainer ruling recorded — Option B: retire
configSchema/PluginConfigValidatorunder ADR-0049Provenance: maintainer, 2026-08-27, PM chat decision-inbox batch 5 (adjudication session
session_01DKWDdUJ2XNRESVVWUvcpnh), verbatim: 「同意」 — accepting recommendation B, presented paired with #12587 and with the ADR-0025 lever stated explicitly (if the plugin distribution layer were near-term, A was the alternative; the maintainer took B).Ruled: retire under ADR-0049 enforce-or-remove — remove
PluginMetadata.configSchema, deletePluginConfigValidator/createPluginConfigValidator(~200 lines plus their unit test), and update ADR-0025 §3.7 to record the retirement: re-declaring a kernel-owned config-validation surface is a fresh decision for the day the distribution layer actually lands, with this card's zero-caller measurement as its starting evidence. Either-way item, binding:ADVANCED_FEATURES.md:304's// Config is validated before init is calledis false on the current ref and dies in the same PR. TS interface field — no Zod tombstone route, no silent-strip hazard; deletion is visible only as a type error to setters, of which there are none.⛔ C stays measured-out (no caller holds a config); A declined (zero pull; ~40 production plugins pass config via constructors and work).
Dispatch notes: Clause-②: yes — removes a published capability from
@objectstack/core's main entry; contract-review tier. Companion #12587 (same interface,hotReloadable) is ruled B in the same batch — the lane may fold the two into one family dispatch if the five fold gates pass (same shape, same package, both ruled, each independently pinnable, exclusion list stated).State:
needs-user-decision→pm:queue.
Generated by Claude Code
Claim: PM loop round 1
Session:session_01LZbWd2jNV1FErXTPSS4Dry
Branch:claude/issue-11982-retire-plugin-config-schema
Worktree:objectstack-issue-11982
Domain:domain:engine
File surface:packages/core/src/plugin-loader.ts,packages/core/src/security/plugin-config-validator.ts(+ its test),packages/core/src/security/index.ts(export barrel),packages/core/ADVANCED_FEATURES.md,docs/adr/0025-plugin-package-distribution.md(stop on breach; explain in the report)
Container & model: M family dispatch,mode:subagent,model: claude-fable-5
Clause-②: yes
Serial constraints cleared:⚠️ NOT fully clear — one live overlap, declared rather than assumed. PR #12652 (#11968, ACCEPTED by this seat, awaiting an enqueue blocked on GraphQL rate limit) editspackages/core/src/security/index.ts, the same barrel this retirement must edit to unpublishPluginConfigValidator. Different regions (an ADR-citation prose line vs an export removal), so this is sequenced, ⛔ not blocked: the dev mergesorigin/mainbefore opening the PR and re-verifies the barrel. No other open PR touches this surface.packages/specis untouched by this card and stays the spec seat's.Chain head of a folded family dispatch: this card + #12587. One branch, one worktree, one changeset, one queue slot; each member keeps its own claim comment, its own
Fixesline and its own commit.Fold gates — all five measured, ⛔ not asserted
Gate Reading ① same defect shape, same fix Both are PluginMetadatafields that are declared, documented, and read by nothing, both ruled retire under ADR-0049 in the same adjudication batch. ⛔ Not "same keyword" — same mechanism.② same package / region Both live in packages/core, both on the same interface,PluginMetadata.③ every member already ruled Both ruled 2026-08-27 ( 5434930483,5434928113-batch). ⛔ Zero unruled members — this gate is the load-bearing one and it is why the fold is legitimate.④ each independently verifiable #11982: configSchemagone +PluginConfigValidator/createPluginConfigValidatordeleted + ADR-0025 §3.7 updated + the false comment dead. #12587:hotReloadablegone +HotReloadManager.reloadPluginno longer implies it. Separate criteria; the batch cannot silently under-deliver.⑤ exclusion list stated ⛔ Do NOT touch these PluginMetadatasiblings, which look like family and are not:startupTimeout— measured READ LIVE (kernel.ts4 hits,kernel.test.ts8) and it is the positive control for the whole zero-read claim;signatureandhealthCheck— not ruled, not measured, not in scope.The ruling — ⛔ not re-adjudicable
Maintainer, 2026-08-27, decision-inbox batch 5, verbatim, ⛔ untranslated:
同意
accepting recommendation B — retire under ADR-0049, presented paired with #12587 and with the ADR-0025 lever stated explicitly (had the plugin distribution layer been near-term, A was the alternative; the maintainer took B).
Binding, from the ruling text: remove
PluginMetadata.configSchema; deletePluginConfigValidator/createPluginConfigValidatorand their unit test; update ADR-0025 §3.7 to record the retirement so re-declaring a kernel-owned config-validation surface is a fresh decision the day the distribution layer lands. Either-way item, binding:ADVANCED_FEATURES.md's// Config is validated before init is calledis false on the current ref and dies in the same PR. ⛔ Option C stays measured-out (no caller holds a config). ⛔ Option A declined (zero pull; ~40 production plugins pass config via constructors and work).🚨 Governed surface — draft only
docs/adr/**is a governed surface. ⛔ Never flip ready, never enqueue, never auto-merge. The PR stays draft for the maintainer to hand-merge; hand-merge is the review record.⚠️ The ADR file isdocs/adr/0025-plugin-package-distribution.md— verified bygit ls-treeonorigin/main; the "ADR-0025-plugin-architecture.md" spelling does not exist and an earlier probe of mine used it wrongly.Premises — measured on
origin/mainat fetch time, with controlsvalidatePluginConfig→ 3 hits inpackages/core/src/plugin-loader.ts, 6 in the validator's own test. No other caller.hotReloadable→ 1 hit inpackages/core/src/security-adjacent core sources: the declaration itself.- Control fired:
startupTimeout→kernel.ts4,kernel.test.ts8. The probe can see a live reader, so the zeros above are readings.
⛔ Confirm or refute each of these before writing code, rather than inheriting them. They were measured by the PM, not by you, and a premise in a brief carries the same burden as a claim in a review.
premise_still_valid: falsewith no PR is a legitimate and valuable delivery.
Generated by Claude Code
Dev claim — folded family dispatch, chain head (#11982 + #12587)
- Session:
session_01LZbWd2jNV1FErXTPSS4Dry— dev seat (os-devsubagent,mode:subagent; the PM claim above is the dispatching seat of the same session, so the session ID matches by design — this comment records that a dev ran) - Branch:
claude/issue-11982-retire-plugin-config-schema(already pushed as write probe) - Worktree:
objectstack-issue-11982 - Comments re-read to the last one in the same action as this claim: no competing dev claim from any other session or branch.
- Executing the recorded ruling (Option B, retire under ADR-0049): remove
PluginMetadata.configSchema, deletePluginConfigValidator/createPluginConfigValidatorand their unit test, unpublish frompackages/core/src/security/index.ts, update ADR-0025 section 3.7, and remove the falseADVANCED_FEATURES.mdvalidation comment. Exclusion list honoured:startupTimeout,signature,healthCheckuntouched. - PR will be draft and must stay draft (
docs/adr/**governed surface — maintainer hand-merge).
Generated by Claude Code
- Session:
os-dev-report
{ "issue": 11982, "family": [11982, 12587], "status": "done", "branch": "claude/issue-11982-retire-plugin-config-schema", "pr": "https://github.com/objectstack-ai/objectstack/pull/12689", "pr_state": "DRAFT and must stay draft — docs/adr/0025-plugin-package-distribution.md is a governed surface; maintainer hand-merge is the review record. Never flip ready, never enqueue, never arm auto-merge.", "premise_still_valid": true, "summary": "Both ruled retirements landed on one branch, one commit per card, plus a pin-relocation commit and a PM-directed docs-drift commit: PluginMetadata.configSchema + PluginConfigValidator/createPluginConfigValidator deleted and unpublished with ADR-0025 section 3.7 recording the retirement and the false ADVANCED_FEATURES sentence dead (#11982); PluginMetadata.hotReloadable removed with its doc line (#12587); three hand-written docs pages aligned. Exclusion list (startupTimeout, signature, healthCheck) untouched; packages/spec untouched; content/docs/releases untouched. Every PM premise re-verified at base 4bd6faa2 before editing — the two extra validatePluginConfig hits the brief did not name (packages/rest) are prose comments, not callers.", "clause2": "yes — removes a published capability from @objectstack/core's main entry. Claim intact: my dev claim comments (5436310822, 5436312507) sit under the PM's on both cards; no competing claim; nothing on either card modified by me except my comments; no unexpected third-party state on the PR (the appended bare footer after the body PATCH is the documented platform behaviour, not another actor).", "files_changed": [ "packages/core/src/plugin-loader.ts", "packages/core/src/security/plugin-config-validator.ts (deleted)", "packages/core/src/security/plugin-config-validator.test.ts (deleted)", "packages/core/src/security/index.ts", "packages/core/ADVANCED_FEATURES.md", "docs/adr/0025-plugin-package-distribution.md", "packages/core/src/plugin-loader.retired-fields.pin.test.ts (new, runtime pins)", "packages/rest/src/plugin-metadata-retired-fields.pin.test.ts (new, compile-time published-surface pins)", ".changeset/retire-plugin-metadata-inert-fields.md (new)", "content/docs/plugins/anatomy.mdx", "content/docs/plugins/index.mdx", "content/docs/protocol/kernel/index.mdx" ], "docs_drift": { "edited": ["content/docs/plugins/anatomy.mdx (retired-field example replaced with constructor-owned config)", "content/docs/plugins/index.mdx (Configuration Validation bullet + example half removed; signature kept — live)", "content/docs/protocol/kernel/index.mdx (Configuration Management snippet + fail-fast callout rewritten to the retirement + self-parse seam)"], "no_change_with_reason": ["content/docs/automation/flows.mdx — its configSchema is the ADR-0018 node-executor schema, different surface", "content/docs/getting-started/quick-reference.mdx — its PluginMetadata is spec plugin-validator.zod.ts's locally-declared homonym, live", "content/docs/protocol/kernel/plugin-spec.mdx — teaches the surviving self-parse pattern; its phantom manifest configSchema file-map row predates this diff, filed as #12690"], "release_owned_read_only": "content/docs/releases/v17.mdx NOT edited. All four configSchema mentions are other surfaces — quoted, line ~532: 'A node's config stays an open record: it is per-node-type, owned by the executor's configSchema and the conversion layer' (ADR-0018); line ~582: 'An earlier version of this note said the driver's own configSchema did that, which was wrong for two releases: the field existed, nothing read it' (driver, historical); ~1949 and ~2242 are flow-designer/ADR-0018. VERDICT: the page is a historical record of shipped, unrelated surfaces; it does not advertise PluginMetadata.configSchema as a v17 capability, so this retirement falsifies nothing there and no separate routing is needed.", "blind_spot_sweep": "hand-written docs grepped for PluginConfigValidator / createPluginConfigValidator / ADVANCED_FEATURES / hotReloadable: zero hits outside releases/. Behavioural phrasings (config validat*, validated before init, hot-reload wording): only connector providerConfig (live, unrelated) and the auto-generated references/ tree (spec HotReloadConfig and the already-tombstoned manifest.loading — regenerated surfaces, not hand-edited). The two no-anchor changed files (ADVANCED_FEATURES.md, security/index.ts) were swept by identifier: no hand-written page mirrors either." }, "gates": { "derived_by": "node scripts/pm/dispatch-gates.mjs, no paths, twice — first at commit 8f6fb6fc, re-derived after the docs pass at 67da0f09 (stderr names objectstack-ai/objectstack both times); union executed in full at a6c83156, docs families at 67da0f09, ratchets + tests re-run at the FINAL head 973e858b", "union_at_a6c83156": "adr-anchors 0 · changeset-gate-self-tests 0 · cross-package-test-inputs 0 · doc-authoring 0 · doc-formula-expressions 0 · kernel-hook-pairs 0 · objectql-double-limit 0 · objectui-changeset 0 · page-declaration-shape 0 · pm-governed-merges 0 · pm-half-states 0 · published-files 0 · slot-lookup 0 · test-source-alias 0 · type-source-resolution 0 · nul-bytes 0 · adr-0087-registration 0 · adr-links 0 · changeset-no-major 0 · ci-filter-parity 0 · comment-mask-adoption 0 · cross-package-test-inputs(ci) 0 · empty-changeset 0 · plugin-teardown-shape 0 · docs-affected 0 · docs-drift-comment 0 · release-rehearsal-self-test 0 · query-options-erasure 0 · type-check-coverage 0 · type-check-debt 0 · engine-double-contract 0 · where-matcher 0 · rest check:test-typecheck 0", "docs_families_at_67da0f09": "doc-frontmatter 0 · doc-route-spelling 0 · docs-section-name 0 · doc-security-posture 0 · spec check:docs 0 · spec check:empty-state 0 · spec check:liveness 0 · spec check:skill-examples 0 · spec check:strictness-ledger 0 · spec check:variant-docs 0 · dispatcher-error-vocabulary 0 · doc-anchors 0 · docs-audit-scope 0 · docs-redirects 0 · docs-single-h1 0 · published-readme-links 0 · react-page-adapter-contract 0 · role-word 0 · doc-authoring 0 · doc-formula-expressions 0 · docs-affected 0 · docs-drift-comment 0 · ci-filter-parity 0 · cross-package-test-inputs 0 · nul-bytes 0 (the final merge 0d4a6a85 brought no content/docs changes, so these readings carry to the final head)", "ratchets_at_final_973e858b": "type-check-coverage 0 · type-check-debt 0 (own line: 31 ledger entries re-measured, 1687 raw errors, none above recorded) · rest check:test-typecheck 0 (own OK line) · engine-double-contract 0 · where-matcher 0 · query-options-erasure 0 · core vitest 0 (40 files / 999 tests passed)", "not_measured": "scripts/pm/check-half-states.mjs exit 3 = PREREQUISITE NOT MET on this seat (container token is a 14-byte proxy placeholder; the tool refuses to sweep anonymously; its own text: the result says NOTHING about the board). NOT green, NOT red — CI runs it with credentials. One earlier batch attempt exited 99 (verify-lock queue-timeout, NOT MEASURED) and was re-run to completion with the kept slot.", "exit_code_discipline": "every gate ran via a runner capturing $? immediately, no pipes before capture; verdicts quoted from each gate's own printed line" }, "tests": "Final head 973e858b: pnpm --filter @objectstack/core test = 'Test Files 40 passed (40)' / 'Tests 999 passed (999)'; rest pin file vitest 1 file / 3 tests passed (at 67da0f09; rest untouched by the final merge); rest check:test-typecheck OK line at 973e858b. Dependency closure built first via turbo under os-verify-lock (VERDICT command-exit 0).", "ablations": [ { "name": "shipping channel — cross-package compile pin (prediction recorded in ablation-prediction-2.txt BEFORE mutation)", "mutation": "re-added 'hotReloadable?: boolean;' to PluginMetadata in packages/core/src/plugin-loader.ts, then REBUILT core dist — the pin reads the built d.ts, so the rebuild is stated and was performed on BOTH legs", "on_disk_confirmation": "src anchor grep 0 pre / 1 post; dist preflight grep on dist/index.d.ts 0 pre-build / 1 post-build (mutation leg) and back to 0 after the restore rebuild (restore leg); control startupTimeout stayed 3 in dist on both legs", "predicted_direction": "MORE diagnostics: check:test-typecheck RED with exactly 1 error in src/plugin-metadata-retired-fields.pin.test.ts (TS2578 unused directive); configSchema directive stays satisfied", "observed": "exactly as predicted — gate exit 1, its own text: 'src/plugin-metadata-retired-fields.pin.test.ts: 1 type error(s) in a file the ledger does not cover'; restore leg: git diff HEAD empty, anchors 0, gate back to its OK line exit 0", "trap": "restore + rebuild in trap EXIT INT TERM with absolute paths" }, { "name": "first channel (superseded residence, evidence kept) — same-package DEBT-ratchet pin", "mutation": "same field re-add, no dist in the loop (tsc reads src); prediction in ablation-prediction.txt", "observed": "tsc error count 98 to 99, the one new error TS2578 at the hotReloadable directive; restore proven by git hash-object equal to the HEAD blob (5bacc628...)" } ], "deviations": [ "Pin residence changed mid-run: check:type-check-coverage refused @ts-expect-error pins in packages/core (no typecheck script — phantom pin; PHANTOM_PIN_DEBT closed to new entries). Compile pins moved to packages/rest whose tsconfig.test.json program is run by its typecheck script and reads core's BUILT d.ts. This adds one test file in packages/rest beyond the claims' declared file surface — declared here per stop-on-breach; no non-test rest file touched.", "Four commits + two merge commits instead of strictly one per card: commit 3 is the pin-relocation repair (first two commits were already pushed; force-push forbidden), commit 4 is the PM-directed docs-drift pass. Each card's substance remains its own commit.", "ADR-0025 edits go slightly beyond the ruled section 3.7 sentence: three factual inventory mentions of the retired validator and the hotReloadable inventory item were aligned so the ADR does not contradict its own record; forward-looking design prose deliberately kept. Enumerated in the PR body for the hand-merge review.", "check:pm-half-states NOT MEASURED on this seat (see gates.not_measured).", "Channel: REST repo-scoped probe answered 200 on this seat, so all GitHub reads/writes rode REST + git; the public-page payload channel covered issue bodies but its front timeline page truncated at 28 items (hasNextPage true), so comments were read via REST. De-dup for the two new filings used complete REST enumeration (326 open issues, pages read to exhaustion, positive control fired); PM's dedup readings were used as standing facts, not re-run." ], "mcp_calls": "0 — no mcp__github__* call was made in this run; every GitHub operation used the container REST channel (probe 200) or git", "open_questions": [], "out_of_scope_findings": [ "filed as #12688: packages/core/REFACTORING_SUMMARY.md section 5 claims config validation that never ran and now names the retired mechanism (unassigned, finding + domain:engine)", "filed as #12690: content/docs/protocol/kernel/plugin-spec.mdx documents a manifest configSchema file-map key ManifestSchema never declared — pre-existing, not falsified by this diff (unassigned, finding + domain:docs)" ] }
Generated by Claude Code
ACCEPT — PR #12689 (family: #11982 + #12587)
Reviewer of record:
domain:engineseat, sessionsession_01LZbWd2jNV1FErXTPSS4Dry(R1). Verified against the diff, ⛔ not against the report's self-description.🚨 Governed surface — this PR does NOT enqueue
docs/adr/0025-plugin-package-distribution.mdis in the diff ⇒ governed. ⛔ Never flip ready, never enqueue, never arm auto-merge. It stays draft for maintainer hand-merge, and the hand-merge is the review record. Confirmed on GitHub:draft: trueat head973e858b.⚠️ Review request will 422 here — this seat's identity isos-zhuang, and GitHub refuses a review request to the PR's own author. Fallback per protocol: assign the PR toos-zhuang, which I will do once CI is green, and name the fallback in the round report.Checklist
Item Reading PR shape Draft, targets main.Part-of PR must not also close its card✅,No other open PR may claim the same issue✅,No other open PR may claim the same single-writer path✅Governed docs/adr/**hit ⇒ hand-merge onlypackages/spec⛔ untouched ✅ — absent from the changed-file list, as required (spec is the spec seat's) content/docs/releases/⛔ untouched ✅ Exclusion list ✅ signature,healthCheck,startupTimeoutall still present inPluginMetadatain the diff.startupTimeoutadditionally serves as the positive control in both pin files.Ruled scope ✅ all four binding items landed: configSchemaremoved ·PluginConfigValidator/createPluginConfigValidator+ unit test deleted and unexported · ADR-0025 §3.7 records the retirement · the false// Config is validated before init is calledexample deletedCI ⏳ running on the final head 973e858b(started 10:28Z). Handoff waits on it.The either-way item is genuinely dead
packages/core/ADVANCED_FEATURES.mdloses the whole "10. Plugin Configuration Validation" section — the example whose inline comment promised "Config is validated before init is called", which was false on the retired ref. ⭐ That sentence was the binding half of the ruling and the retired surface's only in-repo declaration site. It is gone, not softened.Spot-checks
① Both retirements leave a tombstone, not a hole.
plugin-loader.tsreplaces each field with a comment naming the ruling, the date, the reason, and the surviving alternative;security/index.tsreplaces the export block the same way. ⇒ The next reader learns why the field is absent instead of re-adding it.② Both pin files carry a positive control, which is what makes their absence assertions readings:
- runtime pin: the barrel no longer publishes the validator — control: it still publishes
PluginSignatureVerifier/PluginPermissionEnforcer, so the namespace is populated, not accidentally empty; - compile pin:
startupTimeoutstill compiles with no directive, proving the interface accepts real members and the@ts-expect-errordirectives above it are measurements rather than a broken instrument.
③ The pin-residence deviation is the best thing in this PR.
check:type-check-coveragerefused@ts-expect-errorpins inpackages/core— the package has notypecheckscript, so a directive there is a phantom pin: no tsc program that anytypecheckscript runs would ever evaluate it, and it would sit in the tree looking like protection while protecting nothing. The pins moved topackages/rest, whosetsconfig.test.jsonis compiled by itstypecheckscript and resolves@objectstack/coreto the builtdist/index.d.ts⇒ the pin now guards the contract consumers actually see, not an internal source shape.⚠️ Declared cross-lane addition, recorded rather than waved through: that puts one test-only file inpackages/rest, which isdomain:cliterritory and outside this claim's declared file surface. The dev declared it under stop-on-breach and touched no non-test rest file. I accept it: the pin cannot do its job anywhere else, andpackages/restis also the retirement's worked replacement (it parses its own config at its own seam, #11637 — the very pattern the ruling points migrators to). ⛔ Flagged for the hand-merge review rather than buried.④ Ablation proves the pin can fail. Re-adding
hotReloadableand rebuilding core's dist (stated and performed on both legs) turns the directive into TS2578 in a file the ratchet requires at zero errors — observed exactly as predicted, with dist-level anchor greps 0→1 and back, andstartupTimeoutholding at 3 in dist across both legs as the control.⑤ Changeset grade is argued, and the constraint is named.
@objectstack/core: minorwith BREAKING stated in the body —majoris refused repo-wide bycheck:changeset-no-major, so minor-under-the-lockstep-launch-window is the honest encoding rather than a downgrade. It carries per-symbol one-line migrations and the ADR-0087not-requiredannotation with its reasoning.The release-owned page — answered, and the answer is "no action"
I asked for a reading, ⛔ not a decision, and got one.
content/docs/releases/v17.mdxwas read and not edited; all fourconfigSchemamentions are other surfaces: ADR-0018 node-executor config (~532, ~1949, ~2242) and a historical driver note (~582 — "An earlier version of this note said the driver's ownconfigSchemadid that, which was wrong for two releases").⇒ The page does not advertise
PluginMetadata.configSchemaas a v17 capability, so this retirement falsifies nothing there and no separate docs-only PR or issue is owed. ⛔ The guardrail held: a code PR did not touch release notes.Docs drift — 3 edited, 3 declined with reasons
Edited:
plugins/anatomy.mdx(retired-field example → constructor-owned config),plugins/index.mdx(Configuration Validation bullet + example half removed,signaturekept — still live),protocol/kernel/index.mdx(snippet + fail-fast callout rewritten).⭐ Declined with the discrimination I asked for — two of the three are homonyms, exactly the trap:
automation/flows.mdx'sconfigSchemais the ADR-0018 node-executor schema;quick-reference.mdx'sPluginMetadatais spec's locally-declared homonym inplugin-validator.zod.ts, still live.plugin-spec.mdxteaches the surviving self-parse pattern; its phantom manifest row predates this diff and was filed as #12690 rather than fixed here.Blind-spot sweep run over the behavioural phrasings and over both no-anchor files (
ADVANCED_FEATURES.md,security/index.ts) — zero hand-written mirrors.Findings filed, ⛔ not smuggled into this diff
- packages/core/REFACTORING_SUMMARY.md section 5 claims config validation that never ran, and now describes a retired mechanism #12688 —
packages/core/REFACTORING_SUMMARY.md§5 claims config validation that never ran and names the retired mechanism. - content/docs/protocol/kernel/plugin-spec.mdx documents a manifest
configSchemafile-map key that ManifestSchema never declared #12690 —plugin-spec.mdxdocuments a manifestconfigSchemafile-map keyManifestSchemanever declared; pre-existing.
NOT MEASURED, correctly declared
scripts/pm/check-half-states.mjs— exit 3, PREREQUISITE NOT MET (14-byte proxy token placeholder; the tool refuses to sweep anonymously). ⛔ Not green, not red. One earlier batch exited 99 = verify-lock queue timeout, also NOT MEASURED, and was re-run to completion — ⭐ correctly distinguished from a failure, which is the same discipline that mattered on #11966's 240 s timeout.Next action
On an all-green head: assign the PR to
os-zhuangand list it under "awaiting a human merge" in the round report. ⛔ No enqueue, at any point, on this PR.
Generated by Claude Code
- runtime pin: the barrel no longer publishes the validator — control: it still publishes
- added a commit that references this issue
on Sep 1, 2026
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