Empty plugin scopes no longer widen unexpectedly
Plugin loading now correctly preserves empty scope arrays instead of accidentally widening to load all plugins.
When developers explicitly requested no plugins by passing onlyPluginIds: [], the system was treating that empty array the same as not specifying a scope at all. The result was that more plugins loaded than intended — a subtle bug that could cause unexpected behavior in plugin-sensitive code paths.
The fix introduces a shared abstraction for plugin scope semantics. A new module provides functions that correctly distinguish between "no scope specified" (undefined) and "explicitly empty scope" ([]). Instead of scattered inline checks and normalization logic, these helpers are now used consistently across , , , and .
The change touches the plugin loader, runtime, and provider layers to ensure scope is preserved through loaders, metadata snapshots, and cache key generation. Plugin scope handling is now consistent whether the call comes from CLI, API, or runtime hooks.
View Original GitHub Description
Summary
- Problem: explicit plugin scopes were still encoded ad hoc across loader/runtime/provider helpers, so
[]vsundefinedcould drift back into the same behavior outside the first main-path fix. - Why it matters: when an explicitly empty scope widens to unscoped, OpenClaw can load far more plugin surface than the caller asked for.
- What changed: added a shared
plugin-scopehelper and moved loader, provider, runtime metadata, and web-provider cache/scope handling onto it. - What did NOT change (scope boundary): no new activation behavior, no manifest contract changes, and no plugin API changes.
Change Type (select all)
- Bug fix
- Feature
- Refactor required for the fix
- Docs
- Security hardening
- Chore/infra
Scope (select all touched areas)
- Gateway / orchestration
- Skills / tool execution
- Auth / tokens
- Memory / storage
- Integrations
- API / contracts
- UI / DX
- CI/CD / infra
Linked Issue/PR
- Closes #
- Related #65259
- This PR fixes a bug or regression
Root Cause (if applicable)
- Root cause: explicit plugin scope semantics were duplicated in several helpers, and some call sites treated
onlyPluginIds: []the same asonlyPluginIds: undefined. - Missing detection / guardrail: we had coverage for one provider-path regression, but not for the shared loader/runtime/metadata/cache seams that still normalized empty scope away.
- Contributing context (if known): provider narrowing landed incrementally, so the scope contract existed before the shared abstraction did.
Regression Test Plan (if applicable)
- Coverage level that should have caught this:
- Unit test
- Seam / integration test
- End-to-end test
- Existing coverage already sufficient
- Target test or file:
src/plugins/runtime/runtime-registry-loader.test.ts,src/plugins/runtime/metadata-registry-loader.test.ts,src/plugins/providers.test.ts,src/plugins/web-provider-resolution-shared.test.ts - Scenario the test should lock in: explicit empty plugin scopes stay scoped-empty through loaders, metadata snapshots, provider helpers, and web-provider cache/mapping helpers.
- Why this is the smallest reliable guardrail: the bug is in normalization and scope forwarding, not full plugin execution.
- Existing test that already covers this (if any):
src/plugins/loader.runtime-registry.test.tscovers adjacent loader/runtime registry behavior. - If no new test is added, why not: N/A
User-visible / Behavior Changes
None.
Diagram (if applicable)
Before:
[explicit empty scope] -> [helper normalizes to unscoped] -> [broader plugin load]
After:
[explicit empty scope] -> [shared scope helper preserves empty scope] -> [no widening]
Security Impact (required)
- New permissions/capabilities? (
Yes/No) No - Secrets/tokens handling changed? (
Yes/No) No - New/changed network calls? (
Yes/No) No - Command/tool execution surface changed? (
Yes/No) No - Data access scope changed? (
Yes/No) No - If any
Yes, explain risk + mitigation:
Repro + Verification
Environment
- OS: macOS
- Runtime/container: Node 22 / pnpm
- Model/provider: N/A
- Integration/channel (if any): plugin loader/runtime
- Relevant config (redacted): default local plugin test config
Steps
- Request a provider/runtime/metadata load with
onlyPluginIds: []. - Observe whether downstream helpers treat that as explicit empty scope or unscoped.
- Run scoped regression tests and
pnpm build.
Expected
- Explicit empty scopes remain scoped-empty and do not widen plugin loading.
Actual
- This PR preserves that behavior consistently across the touched helpers.
Evidence
Attach at least one:
- Failing test/log before + passing after
- Trace/log snippets
- Screenshot/recording
- Perf numbers (if relevant)
Human Verification (required)
What you personally verified (not just CI), and how:
- Verified scenarios: explicit empty scope handling in runtime registry loading, metadata snapshots, provider helper filtering, web-provider cache keys, loader/runtime/provider regression suites.
- Edge cases checked:
undefinedvs[], scoped-empty cache keys, helper set creation, explicit empty forwarding through metadata/runtime loaders. - What you did not verify: full repo
pnpm test, remote CI, or third-party plugin ecosystems.
Review Conversations
- I replied to or resolved every bot review conversation I addressed in this PR.
- I left unresolved only the conversations that still need reviewer or maintainer judgment.
If a bot review conversation is addressed by this PR, resolve that conversation yourself. Do not leave bot review conversation cleanup for maintainers.
Compatibility / Migration
- Backward compatible? (
Yes/No) Yes - Config/env changes? (
Yes/No) No - Migration needed? (
Yes/No) No - If yes, exact upgrade steps:
Risks and Mitigations
- Risk:
- Scope helper could subtly change existing
undefinedhandling in hot paths. - Mitigation: kept
undefinedsemantics intact, added explicit empty-scope regressions, and ranpnpm buildbecause these are loader/runtime boundaries.
- Scope helper could subtly change existing
AI assistance
- AI-assisted: yes
- Local verification run:
pnpm test:serial src/plugins/runtime/runtime-registry-loader.test.ts src/plugins/runtime/metadata-registry-loader.test.ts src/plugins/providers.test.ts src/plugins/web-provider-resolution-shared.test.ts - Additional local verification run:
pnpm test:serial src/plugins/loader.test.ts src/plugins/loader.runtime-registry.test.ts src/plugins/provider-runtime.test.ts - Additional gates:
pnpm lint -- src/plugins/plugin-scope.ts src/plugins/loader.ts src/plugins/providers.ts src/plugins/providers.runtime.ts src/plugins/runtime/runtime-registry-loader.ts src/plugins/runtime/metadata-registry-loader.ts src/plugins/provider-runtime.ts src/plugins/web-provider-resolution-shared.ts src/plugins/providers.test.ts src/plugins/runtime/runtime-registry-loader.test.ts src/plugins/runtime/metadata-registry-loader.test.ts src/plugins/web-provider-resolution-shared.test.ts,pnpm build