Merged
Size
L
Change Breakdown
Bug Fix60%
Refactor40%
#65298fix(plugins): centralize explicit plugin scope handling

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 [] vs undefined could 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-scope helper 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 as onlyPluginIds: 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.ts covers 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

  1. Request a provider/runtime/metadata load with onlyPluginIds: [].
  2. Observe whether downstream helpers treat that as explicit empty scope or unscoped.
  3. 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: undefined vs [], 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 undefined handling in hot paths.
    • Mitigation: kept undefined semantics intact, added explicit empty-scope regressions, and ran pnpm build because these are loader/runtime boundaries.

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
© 2026 · via Gitpulse