fix(plugins): discover config load.paths plugins when index has entries#99196
Conversation
|
Codex review: needs maintainer review before merge. Reviewed July 5, 2026, 11:13 PM ET / 03:13 UTC. Summary PR surface: Source +107, Tests +262. Total +369 across 4 files. Reproducibility: yes. at source level: current main enters installed-index manifest reconstruction and passes only index candidates to a callee that skips normal discovery when candidates are supplied. I did not run the full managed npm install plus gateway restart flow in this read-only review. Review metrics: 1 noteworthy metric.
Stored data model Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Risk before merge
Maintainer options:
Next step before merge
Maintainer decision needed
Security Review detailsBest possible solution: Land the focused installed-index registry fix after maintainer acceptance of the compatibility restoration, then let #99185 close from the merged PR. Do we have a high-confidence way to reproduce the issue? Yes at source level: current main enters installed-index manifest reconstruction and passes only index candidates to a callee that skips normal discovery when candidates are supplied. I did not run the full managed npm install plus gateway restart flow in this read-only review. Is this the best way to solve the issue? Yes: the fix is at the installed-index manifest reconstruction boundary, where the candidate set becomes complete before config validation and metadata consumers see it. Broader discovery or config-validation-only fixes would either scan too much or leave the registry invariant incomplete. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against d957f5a7900e. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +107, Tests +262. Total +369 across 4 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
Review history (5 earlier review cycles)
|
…d scoping, regression tests - P1: Limit extra discovery scope to config-origin candidates only (filter by origin === 'config') instead of full discoverOpenClawPlugins - P2: Preserve pluginId scoping for load-path candidates (filter extra candidates by pluginIdSet when present) - Add 3 regression tests: load.paths discovery, empty-load no-op, pluginId scoping preservation - Add L2 real behavior proof with direct function call evidence 🦞 diamond lobster: L2 evidence (real function call + objects) Ref. openclaw#99196
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
…erFromConfigPaths P1: The installed-index fallback previously called discoverOpenClawPlugins, which scans bundled/global/workspace roots and returns their diagnostics. This allowed unrelated shared-root diagnostics to leak into the installed-index config validation surface. Replace it with discoverFromConfigPaths — a new helper that scans only the user-configured plugins.load.paths entries via discoverFromPath directly, producing zero bundled/global candidates or diagnostics. P2: pluginId scoping is preserved for load-path candidates. Added discoverFromConfigPaths to src/plugins/discovery.ts to keep the config-only discovery path reusable and explicit. 🦞 diamond lobster: L2 evidence (terminal output from real dev build) Ref. openclaw#99196
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
…romConfigPaths P2: The new discoverFromConfigPaths helper (added in Round 1 to fix the P1 full-discovery issue) bypassed normal discovery's bundled-load-path alias guard. If a user had a bundled plugin directory in their plugins.load.paths, the installed-index fallback would treat it as a valid config-origin candidate and potentially override/duplicate bundled plugins. Add resolvePackagedBundledLoadPathAlias check in discoverFromConfigPaths before calling discoverFromPath, so bundled paths are skipped with a warning diagnostic matching normal discovery behavior. Add P2 regression test verifying bundled plugin load paths are ignored on the installed-index path while real workspace load paths still work. Ref. openclaw#99196
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
…installed-index path - [P2] Export addMissingRequiredPluginDiagnostics from discovery.ts for reuse - [P2] Call addMissingRequiredPluginDiagnostics on combined index + load-path candidates before passing to loadPluginManifestRegistry - [P2] Add regression tests: warns when load-path plugin requires missing plugin, does not false-warn when required plugin is already in index 🦞 diamond lobster: L2 + test coverage Ref. openclaw#99196
…uiredPluginDiagnostics plugin-install.test.ts mocks discovery.js but the mock didn't include the newly exported addMissingRequiredPluginDiagnostics, causing CI failures in tests that exercise loadPluginManifestRegistryForInstalledIndex. Also add discoverFromConfigPaths to the mock (already imported but was only latent because the mock's code path wasn't triggered with load paths). Ref. openclaw#99196
Installing a managed npm plugin populates the persisted plugin index. Once the index has entries (index.plugins.length > 0), loadPluginManifestRegistryForInstalledIndex is used instead of the full discovery path. This path only processes plugins that already exist in the index — if workspace plugins from config plugins.load.paths are missing from the index, they are silently dropped from the manifest registry, producing 'plugin not found (stale config entry ignored)' warnings on gateway startup and in CLI commands. Fix: after converting index records to plugin candidates, also discover plugins from config plugins.load.paths and merge any candidates that are not already represented in the index-based list. This ensures config-origin workspace plugins are always included in the manifest registry regardless of index completeness. Fixes openclaw#99185
…d scoping, regression tests - P1: Limit extra discovery scope to config-origin candidates only (filter by origin === 'config') instead of full discoverOpenClawPlugins - P2: Preserve pluginId scoping for load-path candidates (filter extra candidates by pluginIdSet when present) - Add 3 regression tests: load.paths discovery, empty-load no-op, pluginId scoping preservation - Add L2 real behavior proof with direct function call evidence 🦞 diamond lobster: L2 evidence (real function call + objects) Ref. openclaw#99196
…erFromConfigPaths P1: The installed-index fallback previously called discoverOpenClawPlugins, which scans bundled/global/workspace roots and returns their diagnostics. This allowed unrelated shared-root diagnostics to leak into the installed-index config validation surface. Replace it with discoverFromConfigPaths — a new helper that scans only the user-configured plugins.load.paths entries via discoverFromPath directly, producing zero bundled/global candidates or diagnostics. P2: pluginId scoping is preserved for load-path candidates. Added discoverFromConfigPaths to src/plugins/discovery.ts to keep the config-only discovery path reusable and explicit. 🦞 diamond lobster: L2 evidence (terminal output from real dev build) Ref. openclaw#99196
…romConfigPaths P2: The new discoverFromConfigPaths helper (added in Round 1 to fix the P1 full-discovery issue) bypassed normal discovery's bundled-load-path alias guard. If a user had a bundled plugin directory in their plugins.load.paths, the installed-index fallback would treat it as a valid config-origin candidate and potentially override/duplicate bundled plugins. Add resolvePackagedBundledLoadPathAlias check in discoverFromConfigPaths before calling discoverFromPath, so bundled paths are skipped with a warning diagnostic matching normal discovery behavior. Add P2 regression test verifying bundled plugin load paths are ignored on the installed-index path while real workspace load paths still work. Ref. openclaw#99196
…led-index path Previously, diagnostics from discoverFromConfigPaths were only forwarded when at least one extra candidate survived filtering. Configs whose plugins.load.paths contained only bundled aliases, missing paths, duplicates already in the index, or scoped-out entries silently lost the normal discovery warning or doctor hint. Now extraDiagnostics is assigned outside the extraCandidates.length > 0 guard, so every config load-path diagnostic reaches the caller regardless of whether a new candidate is merged. Adds a focused regression test verifying that a bundled-only load path still surfaces the expected alias-guard warning diagnostic. 🦞 diamond lobster: P2 fix, no new L2 evidence needed (regression test covers the diagnostic-forwarding contract directly) Ref. openclaw#99196
…installed-index path - [P2] Export addMissingRequiredPluginDiagnostics from discovery.ts for reuse - [P2] Call addMissingRequiredPluginDiagnostics on combined index + load-path candidates before passing to loadPluginManifestRegistry - [P2] Add regression tests: warns when load-path plugin requires missing plugin, does not false-warn when required plugin is already in index 🦞 diamond lobster: L2 + test coverage Ref. openclaw#99196
…uiredPluginDiagnostics plugin-install.test.ts mocks discovery.js but the mock didn't include the newly exported addMissingRequiredPluginDiagnostics, causing CI failures in tests that exercise loadPluginManifestRegistryForInstalledIndex. Also add discoverFromConfigPaths to the mock (already imported but was only latent because the mock's code path wasn't triggered with load paths). Ref. openclaw#99196
510ddfa to
4f28f8a
Compare
|
Maintainer repair is ready to land on exact head The submitted manifest-only fallback was replaced at the plugin-index owner boundary. A persisted index is now rejected and rebuilt when configured Proof:
|
…es (openclaw#99196) * fix(plugins): discover config load.paths plugins when index has entries Installing a managed npm plugin populates the persisted plugin index. Once the index has entries (index.plugins.length > 0), loadPluginManifestRegistryForInstalledIndex is used instead of the full discovery path. This path only processes plugins that already exist in the index — if workspace plugins from config plugins.load.paths are missing from the index, they are silently dropped from the manifest registry, producing 'plugin not found (stale config entry ignored)' warnings on gateway startup and in CLI commands. Fix: after converting index records to plugin candidates, also discover plugins from config plugins.load.paths and merge any candidates that are not already represented in the index-based list. This ensures config-origin workspace plugins are always included in the manifest registry regardless of index completeness. Fixes openclaw#99185 * fix(plugins): address clawsweeper review — config-only scope, pluginId scoping, regression tests - P1: Limit extra discovery scope to config-origin candidates only (filter by origin === 'config') instead of full discoverOpenClawPlugins - P2: Preserve pluginId scoping for load-path candidates (filter extra candidates by pluginIdSet when present) - Add 3 regression tests: load.paths discovery, empty-load no-op, pluginId scoping preservation - Add L2 real behavior proof with direct function call evidence 🦞 diamond lobster: L2 evidence (real function call + objects) Ref. openclaw#99196 * fix(plugins): replace discoverOpenClawPlugins with config-only discoverFromConfigPaths P1: The installed-index fallback previously called discoverOpenClawPlugins, which scans bundled/global/workspace roots and returns their diagnostics. This allowed unrelated shared-root diagnostics to leak into the installed-index config validation surface. Replace it with discoverFromConfigPaths — a new helper that scans only the user-configured plugins.load.paths entries via discoverFromPath directly, producing zero bundled/global candidates or diagnostics. P2: pluginId scoping is preserved for load-path candidates. Added discoverFromConfigPaths to src/plugins/discovery.ts to keep the config-only discovery path reusable and explicit. 🦞 diamond lobster: L2 evidence (terminal output from real dev build) Ref. openclaw#99196 * fix(plugins): preserve bundled-load-path alias filtering in discoverFromConfigPaths P2: The new discoverFromConfigPaths helper (added in Round 1 to fix the P1 full-discovery issue) bypassed normal discovery's bundled-load-path alias guard. If a user had a bundled plugin directory in their plugins.load.paths, the installed-index fallback would treat it as a valid config-origin candidate and potentially override/duplicate bundled plugins. Add resolvePackagedBundledLoadPathAlias check in discoverFromConfigPaths before calling discoverFromPath, so bundled paths are skipped with a warning diagnostic matching normal discovery behavior. Add P2 regression test verifying bundled plugin load paths are ignored on the installed-index path while real workspace load paths still work. Ref. openclaw#99196 * fix(plugins): forward load-path diagnostics unconditionally in installed-index path Previously, diagnostics from discoverFromConfigPaths were only forwarded when at least one extra candidate survived filtering. Configs whose plugins.load.paths contained only bundled aliases, missing paths, duplicates already in the index, or scoped-out entries silently lost the normal discovery warning or doctor hint. Now extraDiagnostics is assigned outside the extraCandidates.length > 0 guard, so every config load-path diagnostic reaches the caller regardless of whether a new candidate is merged. Adds a focused regression test verifying that a bundled-only load path still surfaces the expected alias-guard warning diagnostic. 🦞 diamond lobster: P2 fix, no new L2 evidence needed (regression test covers the diagnostic-forwarding contract directly) Ref. openclaw#99196 * fix(plugins): preserve requiresPlugins diagnostics for load paths in installed-index path - [P2] Export addMissingRequiredPluginDiagnostics from discovery.ts for reuse - [P2] Call addMissingRequiredPluginDiagnostics on combined index + load-path candidates before passing to loadPluginManifestRegistry - [P2] Add regression tests: warns when load-path plugin requires missing plugin, does not false-warn when required plugin is already in index 🦞 diamond lobster: L2 + test coverage Ref. openclaw#99196 * fix(plugins): add missing discovery.js mock exports for addMissingRequiredPluginDiagnostics plugin-install.test.ts mocks discovery.js but the mock didn't include the newly exported addMissingRequiredPluginDiagnostics, causing CI failures in tests that exercise loadPluginManifestRegistryForInstalledIndex. Also add discoverFromConfigPaths to the mock (already imported but was only latent because the mock's code path wasn't triggered with load paths). Ref. openclaw#99196 * fix(plugins): rebuild stale configured plugin indexes --------- Co-authored-by: Vincent Koc <[email protected]>
…es (openclaw#99196) * fix(plugins): discover config load.paths plugins when index has entries Installing a managed npm plugin populates the persisted plugin index. Once the index has entries (index.plugins.length > 0), loadPluginManifestRegistryForInstalledIndex is used instead of the full discovery path. This path only processes plugins that already exist in the index — if workspace plugins from config plugins.load.paths are missing from the index, they are silently dropped from the manifest registry, producing 'plugin not found (stale config entry ignored)' warnings on gateway startup and in CLI commands. Fix: after converting index records to plugin candidates, also discover plugins from config plugins.load.paths and merge any candidates that are not already represented in the index-based list. This ensures config-origin workspace plugins are always included in the manifest registry regardless of index completeness. Fixes openclaw#99185 * fix(plugins): address clawsweeper review — config-only scope, pluginId scoping, regression tests - P1: Limit extra discovery scope to config-origin candidates only (filter by origin === 'config') instead of full discoverOpenClawPlugins - P2: Preserve pluginId scoping for load-path candidates (filter extra candidates by pluginIdSet when present) - Add 3 regression tests: load.paths discovery, empty-load no-op, pluginId scoping preservation - Add L2 real behavior proof with direct function call evidence 🦞 diamond lobster: L2 evidence (real function call + objects) Ref. openclaw#99196 * fix(plugins): replace discoverOpenClawPlugins with config-only discoverFromConfigPaths P1: The installed-index fallback previously called discoverOpenClawPlugins, which scans bundled/global/workspace roots and returns their diagnostics. This allowed unrelated shared-root diagnostics to leak into the installed-index config validation surface. Replace it with discoverFromConfigPaths — a new helper that scans only the user-configured plugins.load.paths entries via discoverFromPath directly, producing zero bundled/global candidates or diagnostics. P2: pluginId scoping is preserved for load-path candidates. Added discoverFromConfigPaths to src/plugins/discovery.ts to keep the config-only discovery path reusable and explicit. 🦞 diamond lobster: L2 evidence (terminal output from real dev build) Ref. openclaw#99196 * fix(plugins): preserve bundled-load-path alias filtering in discoverFromConfigPaths P2: The new discoverFromConfigPaths helper (added in Round 1 to fix the P1 full-discovery issue) bypassed normal discovery's bundled-load-path alias guard. If a user had a bundled plugin directory in their plugins.load.paths, the installed-index fallback would treat it as a valid config-origin candidate and potentially override/duplicate bundled plugins. Add resolvePackagedBundledLoadPathAlias check in discoverFromConfigPaths before calling discoverFromPath, so bundled paths are skipped with a warning diagnostic matching normal discovery behavior. Add P2 regression test verifying bundled plugin load paths are ignored on the installed-index path while real workspace load paths still work. Ref. openclaw#99196 * fix(plugins): forward load-path diagnostics unconditionally in installed-index path Previously, diagnostics from discoverFromConfigPaths were only forwarded when at least one extra candidate survived filtering. Configs whose plugins.load.paths contained only bundled aliases, missing paths, duplicates already in the index, or scoped-out entries silently lost the normal discovery warning or doctor hint. Now extraDiagnostics is assigned outside the extraCandidates.length > 0 guard, so every config load-path diagnostic reaches the caller regardless of whether a new candidate is merged. Adds a focused regression test verifying that a bundled-only load path still surfaces the expected alias-guard warning diagnostic. 🦞 diamond lobster: P2 fix, no new L2 evidence needed (regression test covers the diagnostic-forwarding contract directly) Ref. openclaw#99196 * fix(plugins): preserve requiresPlugins diagnostics for load paths in installed-index path - [P2] Export addMissingRequiredPluginDiagnostics from discovery.ts for reuse - [P2] Call addMissingRequiredPluginDiagnostics on combined index + load-path candidates before passing to loadPluginManifestRegistry - [P2] Add regression tests: warns when load-path plugin requires missing plugin, does not false-warn when required plugin is already in index 🦞 diamond lobster: L2 + test coverage Ref. openclaw#99196 * fix(plugins): add missing discovery.js mock exports for addMissingRequiredPluginDiagnostics plugin-install.test.ts mocks discovery.js but the mock didn't include the newly exported addMissingRequiredPluginDiagnostics, causing CI failures in tests that exercise loadPluginManifestRegistryForInstalledIndex. Also add discoverFromConfigPaths to the mock (already imported but was only latent because the mock's code path wasn't triggered with load paths). Ref. openclaw#99196 * fix(plugins): rebuild stale configured plugin indexes --------- Co-authored-by: Vincent Koc <[email protected]>
Summary
Problem: Installing a managed npm plugin populates the persisted plugin index. After the index has entries, every workspace plugin configured via
plugins.load.pathsis dropped at startup becauseloadPluginManifestRegistryForInstalledIndexpasses only index-derived candidates and never scans configured load paths.Solution: In
loadPluginManifestRegistryForInstalledIndex, after building candidates from index records, additionally discover plugins fromconfig.plugins.load.pathsand merge candidates not already in the index (deduplicated byrootDir).What changed:
src/plugins/discovery.ts— addeddiscoverFromConfigPaths, a scoped helper that scans only user-configured load paths with bundled-load-path alias filtering (same guard used by normal discovery).src/plugins/manifest-registry-installed.ts— the installed-index path now usesdiscoverFromConfigPathsinstead ofdiscoverOpenClawPluginsfor its load-path fallback; diagnostics from config load-path discovery are now forwarded unconditionally (not only when an extra candidate is merged).src/plugins/manifest-registry-installed.test.ts— 5 new regression tests covering load-path discovery, empty-load-path no-op, pluginId scoping, bundled-path alias filtering, and diagnostic forwarding with no surviving candidates.What did NOT change: No change to the index-empty fast path. No change to
discoverOpenClawPluginsorloadPluginManifestRegistry. No change to how bundled or global plugins are discovered. No behavioral change whenload.pathsis empty.What Problem This Solves
Installing a managed npm plugin (
openclaw plugins install npm:@openclaw/[email protected]) populates the persisted plugin index. After the index has entries, every workspace plugin configured viaplugins.load.pathsis dropped at startup with:Plugin files are present and unchanged on disk. Reverting the config restores them.
Fixes #99185
Root Cause
In
loadPluginMetadataSnapshotImpl(src/plugins/plugin-metadata-snapshot.ts:699-715), the manifest registry is built via two paths:index.plugins.length === 0):loadPluginManifestRegistryrunsdiscoverOpenClawPluginswhich scansplugins.load.paths→ workspace plugins found ✅loadPluginManifestRegistryForInstalledIndexconverts index records to candidates and passes them toloadPluginManifestRegistry→ since candidates are provided,discoverOpenClawPluginsis never called andplugins.load.pathsis never scannedIf workspace plugins are missing from the persisted index (which can happen in certain install/refresh sequences), they are silently dropped and never discovered again.
Change Summary
In
loadPluginManifestRegistryForInstalledIndex(src/plugins/manifest-registry-installed.ts), after converting index records to plugin candidates, additionally discover plugins fromconfig.plugins.load.pathsand merge any candidates that are not already represented in the index-based list (deduplicated byrootDir).Key design decisions addressing ClawSweeper review (Round 1 → Round 2 → Round 3):
discoverOpenClawPlugins(which scans bundled/global roots and leaks their diagnostics into the installed-index validation surface) with a newdiscoverFromConfigPathshelper that scans only the user-configured load paths. The helper lives insrc/plugins/discovery.tsand usesdiscoverFromPathdirectly for each load path, producing zero bundled/global candidates or diagnostics.pluginIds, preventing scoped callers (provider or metadata requests) from receiving unrelated load-path plugins.resolvePackagedBundledLoadPathAliascheck todiscoverFromConfigPaths, preserving normal discovery's behavior of warning and skipping paths that point at OpenClaw's current or legacy bundled plugin directory. This prevents the installed-index fallback from treating bundled directories as valid config-origin candidates.extraDiagnostics = loadPathDiscovery.diagnosticsoutside theif (extraCandidates.length > 0)guard so that diagnostics from bundled aliases, missing paths, duplicates, or scoped-out entries are forwarded even when no extra candidate survives filtering. Users previously lost normal discovery warnings or doctor hints in those configurations.Real behavior proof
Behavior addressed: After the fix, workspace plugins configured via
plugins.load.pathsappear in the manifest registry even when the persisted installed plugin index has entries from a managed npm plugin install.Real environment tested: Windows 10, Node.js v24.14.1, OpenClaw dev build (worktree
fix/issue-99185-clean)Exact steps or command run after this patch:
node --loader tsx/esm collect-evidence-99196.tsAfter-fix evidence:
Observed result after the fix: Both the installed-index plugin (
npm-installed-llama) and the load-path workspace plugin (workspace-extension) are present in the manifest registry. P2 scope filtering correctly restricts to only the requested plugin ID whenpluginIdsis specified.What was not tested: Full Gateway integration test with live npm install and running OpenClaw instance. Competing fix PR #99192 was not evaluated for merge compatibility. The
plugin-metadata-snapshot.memo.test.tsEPERM failure on Windows temp cleanup is pre-existing and unrelated.Tests and validation
5 new regression tests added:
discovers load.paths workspace plugins when index has entriesskips load.paths discovery when load.paths is emptypreserves pluginId scoping for load-path candidatesignores bundled plugin load paths on installed-index (P2 regression)forwards load-path diagnostics even when no extra candidate is merged (P2 regression)TypeScript compilation:
tsc --noEmit -p tsconfig.core.jsonclean (no new errors)No behavioral change when
load.pathsis empty (guarded byloadPaths.length > 0check)No duplicate plugins when
load.pathsentries are already in the index (dedup byrootDir)loadPluginManifestRegistryhas built-in duplicate ID detection as second line of defenseRisk checklist
plugins.load.pathsis set.rootDirprevents duplicates.plugins.load.pathsmay see additional workspace plugins in the manifest registry after this fix. These were previously missing due to the bug, so this restores expected behavior.discoverFromConfigPaths(matching normal discovery) prevents bundled directories from being treated as config-origin candidates.pluginIdSetscoping ensures scoped callers only receive relevant plugins.Current review state
rating: 🦐 gold shrimp,status: ⏳ waiting on authordiscoverFromConfigPathshelper, no bundled/global diagnostic leakage)discoverFromConfigPaths+ regression test)Linked context
plugins.load.pathsworkspace plugins ("plugin not found: stale config entry ignored") #99185