openclaw - ✅(Solved) Fix [Bug]: Path-based plugin tools (origin "config") not exposed after 2026.5.2 [4 pull requests, 2 comments, 3 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#76598Fetched 2026-05-04 05:04:56
View on GitHub
Comments
2
Participants
3
Timeline
18
Reactions
2
Timeline (top)
cross-referenced ×6referenced ×5commented ×2labeled ×2

Summary

After upgrading from v2026.4.29 to v2026.5.2, agent tools registered by path-based plugins (loaded via plugins.load.paths, manifest origin "config") silently fail to appear in agent tool palettes. The plugin's register() callback is invoked at gateway startup, but the tool factory is never called when an agent resolves its tool list, so the gateway returns unknown method errors when the agent tries to call the tool.

This is the same regression family that PR #76536 fixes for capability providers and PR #76393 fixed for memory-plugin doctor/status — the on-demand load fallback that PR #76004
("perf(plugins): reuse startup runtime registry") removed from resolvePluginToolRegistry in src/plugins/tools.ts is also needed for tool resolution.

The Codex review on PR #76004 explicitly flagged this exact risk before merge:

"Keep plugin tools from disappearing on cold registries — resolvePluginTools now only reads the loaded channel registry."

Error Message

error Gateway call failed: GatewayClientRequestError: unknown method: <tool-name>.run info gateway/ws ⇄ res ✗ <tool-name>.run 0ms errorCode=INVALID_REQUEST errorMessage=unknown method: <tool-name>.run

Root Cause

Root cause

Fix Action

Fix / Workaround

  • PR #76536 "fix(plugins): preserve external capability provider fallback" (open, ready to merge): patches capability-provider-runtime.ts. Description quotes verbatim:

    "preserve the v2026.5.2 fast path that reuses an already-loaded runtime registry, but fall back to the scoped manifest-derived runtime load when that registry is missing the provider."

    • PR #76393 "fix(cli): load memory plugin for doctor/status when registry is cold" (merged): patches the doctor/status command path with the same scoped fallback.

Neither patches src/plugins/tools.ts, so the tool resolution surface — used by every agent on every turn — is still broken.

Workaround (in production today)

PR fix notes

PR #76609: fix(plugins): restore cold-registry load for path-based plugin tools (#76598)

Description (problem / solution / changelog)

Summary

resolvePluginTools silently returned an empty list when no pre-warmed channel/active runtime registry was available — specifically for path-based plugins loaded via plugins.load.paths (origin "config").

Root cause: PR #76004 ("perf(plugins): reuse startup runtime registry") replaced resolveRuntimePluginRegistry (which did on-demand loading) with getLoadedRuntimePluginRegistry (lookup-only). The on-demand load fallback was restored for memory plugins (#76393) and capability providers (#76536), but not for resolvePluginTools. Result: path-based plugin tool factories register at startup but their tools disappear at agent request time with unknown method: <tool-name>.run.

Fix: When resolvePluginToolRegistry returns null (cold registry), call ensureStandaloneRuntimePluginRegistryLoaded to trigger the load, then retry. If still null after load, return the (possibly partial) cached tool list.

Changes

  • src/plugins/tools.ts: add cold-registry fallback load + retry in resolvePluginTools
  • src/plugins/tools.optional.test.ts: regression test asserting tools resolve without pre-warming the registry
  • CHANGELOG.md: fix entry

Audit

RuleCheckResult
A: existing helper?ensureStandaloneRuntimePluginRegistryLoaded already imported in tools.tsreused
B: shared callers ≥3?resolvePluginTools is called from many sites, but the change is internal to its bodyno contract change
C: rival PR?No open PR mentioning #76598 or touching src/plugins/tools.ts for this pathNONE

Test

pnpm test src/plugins/tools.optional.test.ts
# 38/38 pass

Fixes #76598.

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • src/plugins/tools.optional.test.ts (modified, +108/-2)
  • src/plugins/tools.ts (modified, +42/-2)

PR #76636: fix(plugins): include selected context-engine slot plugin in gateway startup (#76576)

Description (problem / solution / changelog)

Root cause

resolveGatewayStartupPluginPlanFromRegistry builds the startup plugin set by filtering installed plugins. For memory plugins it uses shouldConsiderForGatewayStartup which checks activation.onStartup or kind: "memory". External context-engine plugins (e.g. [email protected]) have kind: "context-engine" but no activation.onStartup in their manifest — they register via api.registerContextEngine() at load time. The planner skipped them, so the selected engine was never loaded before agent turns resolved the active engine, producing:

[context-engine] Context engine "lossless-claw" is not registered; falling back to default engine "legacy".

Fix

Add resolveContextEngineSlotStartupPluginId mirroring the existing resolveMemorySlotStartupPluginId pattern. Pass contextEngineSlotStartupPluginId into shouldConsiderForGatewayStartup so any plugin matching plugins.slots.contextEngine is included in the gateway startup load plan regardless of its manifest activation shape.

Audit:

  • A: resolveMemorySlotStartupPluginId is the existing helper — mirrored, not reused (different semantics: context engine has no built-in default override, no dreaming sub-case)
  • B: shouldConsiderForGatewayStartup has one non-test caller — change is purely additive, no contract change
  • C: No rival PR found

Tests

4 new cases in channel-plugin-ids.test.ts:

  • Includes selected context-engine plugin without activation.onStartup (regression for #76576)
  • Does not include unselected context-engine plugins even when enabled
  • Skips startup when slot is set to built-in "legacy" engine
  • Normalizes the slot id before filtering

82/82 tests pass (src/plugins/channel-plugin-ids.test.ts src/plugins/effective-plugin-ids.test.ts src/agents/runtime-plugins.test.ts).

Fixes #76576. Thanks @hclsys.

Changed files

  • CHANGELOG.md (modified, +2/-0)
  • src/plugins/channel-plugin-ids.test.ts (modified, +59/-5)
  • src/plugins/gateway-startup-plugin-ids.ts (modified, +38/-3)
  • src/plugins/tools.optional.test.ts (modified, +108/-2)
  • src/plugins/tools.ts (modified, +42/-2)

PR #76536: fix(plugins): preserve external capability provider fallback

Description (problem / solution / changelog)

Summary

  • Problem: after v2026.5.2, enabled external capability providers declared only through manifest contracts (for example contracts.speechProviders) can fail with no provider registered unless they are also startup-loaded.
  • Why it matters: docs describe manifest ownership/contract fallback as preserving correctness; missing startup activation should cost performance, not make an installed provider unusable.
  • What changed: preserve the v2026.5.2 fast path that reuses an already-loaded runtime registry, but fall back to the scoped manifest-derived runtime load when that registry is missing the provider.
  • What did NOT change (scope boundary): this does not change plugin activation policy, startup sidecar selection, bundled provider compatibility capture, or provider config semantics.

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 N/A
  • Related N/A
  • This PR fixes a bug or regression

Root Cause (if applicable)

  • Root cause: commit 8283c5d6cc changed capability provider fallback loading from resolveRuntimePluginRegistry(params.loadOptions) to getLoadedRuntimePluginRegistry(...). That preserved startup-registry reuse, but dropped the cold-load path for enabled external providers that are declared in manifest contracts but absent from the startup registry.
  • Missing detection / guardrail: no regression test covered an enabled external manifest-contract provider that is not startup-loaded.
  • Contributing context (if known): Fish Audio @conan-scott/[email protected] declares contracts.speechProviders: ["fish-audio"]; without activation.onStartup, v2026.5.2 could discover the contract but failed to register the runtime provider on request.

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/capability-provider-runtime.test.ts
  • Scenario the test should lock in: an enabled external speech provider declared via contracts.speechProviders is absent from the startup registry, then resolves through the scoped cold-load path.
  • Why this is the smallest reliable guardrail: the regression is in capability-provider runtime selection, before provider-specific TTS behavior matters.
  • Existing test that already covers this (if any): none found.
  • If no new test is added, why not: N/A; a regression test is added.

User-visible / Behavior Changes

Enabled external capability providers declared through manifest contracts can again resolve on request without requiring activation.onStartup for correctness.

Diagram (if applicable)

Before:
TTS request -> manifest contract identifies external provider -> startup registry missing provider -> no provider registered

After:
TTS request -> manifest contract identifies external provider -> startup registry missing provider -> scoped cold load -> provider registered

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: N/A

Repro + Verification

Environment

  • OS: Linux container / OpenShift pod (linux 6.12.0-153.el10.x86_64, Node 24.14.0)
  • Runtime/container: OpenClaw 2026.5.2
  • Model/provider: N/A
  • Integration/channel (if any): TTS via Fish Audio speech provider
  • Relevant config (redacted): Fish Audio installed and enabled; installed manifest declares contracts.speechProviders: ["fish-audio"]; activation.onStartup removed for the repro.

Steps

  1. Install/enable @conan-scott/[email protected] with contracts.speechProviders: ["fish-audio"] and no activation.onStartup.
  2. Hard-restart the OpenClaw pod on unpatched 2026.5.2.
  3. Run a TTS request using Fish Audio.
  4. Apply this PR's runtime change as a loader monkey patch only; keep the Fish plugin manifest unpatched.
  5. Hard-restart the pod again.
  6. Run the same TTS request.

Expected

  • The enabled provider declared by contracts.speechProviders should be request-loadable and usable.

Actual

  • Before patch: TTS failed with fish-audio: no provider registered; microsoft: not configured; minimax: not configured.
  • After patch: the same Fish Audio TTS path worked after hard pod restart with no plugin-side activation patch.

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Local automated checks:

  • pnpm exec vitest run src/plugins/capability-provider-runtime.test.ts — 37 passed
  • pnpm exec oxfmt --check src/plugins/capability-provider-runtime.ts src/plugins/capability-provider-runtime.test.ts — passed
  • pnpm exec oxlint src/plugins/capability-provider-runtime.ts src/plugins/capability-provider-runtime.test.ts — 0 warnings / 0 errors

Manual runtime evidence:

  • Baseline hard restart, Fish manifest without activation.onStartup, no runtime patch: fish-audio: no provider registered; microsoft: not configured; minimax: not configured.
  • Monkey-patched runtime with this PR's code, Fish manifest still without activation.onStartup, hard restarted pod: /tts audio exact reinstall hard restart test after monkey patch worked.

Human Verification (required)

What you personally verified (not just CI), and how:

  • Verified scenarios: baseline failure on unpatched 2026.5.2; success after applying only this runtime fallback as a loader monkey patch; hard pod restart on both sides.
  • Edge cases checked: Fish plugin manifest intentionally left without activation.onStartup during the passing test, so success came from runtime fallback rather than plugin startup activation.
  • What you did not verify: a full packaged OpenClaw image built from this branch; broader non-speech providers beyond the unit coverage.

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: N/A

Risks and Mitigations

  • Risk: reintroducing cold provider loads could partially reduce the startup-registry reuse optimization from v2026.5.2.
    • Mitigation: the cold load only runs when no compatible loaded registry is available for the scoped provider request; the fast path remains first.

Changed files

  • CHANGELOG.md (modified, +2/-0)
  • src/plugins/capability-provider-runtime.test.ts (modified, +58/-1)
  • src/plugins/capability-provider-runtime.ts (modified, +17/-3)

PR #76713: fix(media-understanding): use createRequire instead of import() for sharp (CJS-only module)

Description (problem / solution / changelog)

Problem

In Node 24+, import("sharp") fails because sharp is a CJS-only package:

  • Its package.json has "type": "commonjs" and no ESM exports field
  • Node ESM resolution looks for sharp/index.js but sharp only has sharp/lib/index.js
  • The type import for sharp works (TS type-only import), but the runtime import() fails with Cannot find package error

This causes Failed to optimize image: Optional dependency sharp is required for image attachment processing when users send images to the agent.

Fix

Replace the dynamic import(SHARP_MODULE) with createRequire("sharp") from "node:module". This is already used elsewhere in the codebase (e.g. Discord extension audio, tests) for loading CJS modules in ESM context.

This removes the normalizeSharpFactory function that was only needed for unwrapping ESM import results.

Testing

  • Verified on Node v24.15.0 + [email protected] + macOS arm64
  • Image analysis works correctly after the fix

Closes #TBD

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • extensions/media-understanding-core/image-ops.ts (modified, +16/-32)

Code Example

error Gateway call failed: GatewayClientRequestError: unknown method: <tool-name>.run
  info gateway/ws ⇄ res ✗ <tool-name>.run 0ms errorCode=INVALID_REQUEST errorMessage=unknown method: <tool-name>.run

---

{                                                           
    "name": "@example/dummy-test",                                                                                                                                                      
    "version": "0.1.0",
    "type": "module",
    "openclaw": {
      "extensions": ["./index.ts"],
      "compat": { "pluginApi": ">=2026.3.24-beta.2" }                                                                                                                                     
    }                                                                                                                                                                                     
  }

---

{
    "id": "dummy-test",
    "name": "Dummy Test",
    "description": "Minimal plugin to verify path-based tool plugins work.",                                                                                                              
    "activation": { "onStartup": true },
    "contracts": { "tools": ["dummy-test"] },                                                                                                                                             
    "configSchema": {                                         
      "type": "object",                                                                                                                                                                   
      "additionalProperties": false,                          
      "properties": {}                                                                                                                                                                  
    }
  }

---

import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
  import type {                                                                                                                                                                           
    AnyAgentTool,                                                                                                                                                                         
    OpenClawPluginApi,                                                                                                                                                                    
    OpenClawPluginToolFactory,                                                                                                                                                            
  } from "openclaw/plugin-sdk/plugin-entry";                                                                                                                                              
                                                                                                                                                                                        
  export default definePluginEntry({                                                                                                                                                      
    id: "dummy-test",                                         
    name: "Dummy Test",                                                                                                                                                                   
    description: "Minimal A/B test plugin",
                                                                                                                                                                                          
    register(api: OpenClawPluginApi) {                        
      api.logger?.info("[dummy-test:DIAG] register() ENTERED");                                                                                                                           
                                                                                                                                                                                          
      api.registerTool(                                                                                                                                                                   
        ((ctx) => {                                                                                                                                                                       
          api.logger?.info(                                                                                                                                                               
            `[dummy-test:DIAG] FACTORY called sandboxed=${ctx.sandboxed}`,                                                                                                              
          );                                                                                                                                                                              
          if (ctx.sandboxed) return null;                     
          const tool: AnyAgentTool = {                                                                                                                                                    
            name: "dummy-test",                               
            label: "Dummy Test",                                                                                                                                                          
            description: "Returns 'pong' to verify tool exposure.",                                                                                                                       
            parameters: { type: "object", properties: {}, additionalProperties: false },                                                                                                  
            async execute() {                                                                                                                                                             
              return {                                                                                                                                                                    
                content: [{ type: "text" as const, text: "pong" }],                                                                                                                     
                details: { ok: true },                                                                                                                                                    
              };                                                                                                                                                                          
            },                                                                                                                                                                          
          } as unknown as AnyAgentTool;                                                                                                                                                   
          return tool;                                        
        }) as OpenClawPluginToolFactory,                                                                                                                                                
        { optional: true },
      );                                                                                                                                                                                  
   
      api.logger?.info("[dummy-test:DIAG] register() EXITED");                                                                                                                            
    },                                                        
  });

---

{
    "plugins": {
      "allow": ["dummy-test"],
      "load": { "paths": ["/path/to/dummy-test"] },                                                                                                                                       
      "entries": { "dummy-test": { "enabled": true } }
    },                                                                                                                                                                                    
    "tools": { "allow": ["dummy-test", "group:plugins"] }     
  }

---

[dummy-test:DIAG] register() ENTERED                        
  [dummy-test:DIAG] register() EXITED                                                                                                                                                   
  [dummy-test:DIAG] FACTORY called sandboxed=false  ← appears at first agent request

---

[dummy-test:DIAG] register() ENTERED                        
  [dummy-test:DIAG] register() EXITED                                                                                                                                                   
  # ... agent requests come and go, FACTORY never called ...

---

[dummy-test:DIAG] register() ENTERED                        
  [dummy-test:DIAG] register() EXITED                                                                                                                                                   
  [dummy-test:DIAG] FACTORY called sandboxed=false  ← appears at first agent request

---

error Gateway call failed: GatewayClientRequestError: unknown method: <tool-name>.run
  info gateway/ws ⇄ res ✗ <tool-name>.run 0ms errorCode=INVALID_REQUEST errorMessage=unknown method: <tool-name>.run

---

{                                                           
    "name": "@example/dummy-test",                                                                                                                                                      
    "version": "0.1.0",
    "type": "module",
    "openclaw": {
      "extensions": ["./index.ts"],
      "compat": { "pluginApi": ">=2026.3.24-beta.2" }                                                                                                                                     
    }                                                                                                                                                                                     
  }

---

{
    "id": "dummy-test",
    "name": "Dummy Test",
    "description": "Minimal plugin to verify path-based tool plugins work.",                                                                                                              
    "activation": { "onStartup": true },
    "contracts": { "tools": ["dummy-test"] },                                                                                                                                             
    "configSchema": {                                         
      "type": "object",                                                                                                                                                                   
      "additionalProperties": false,                          
      "properties": {}                                                                                                                                                                  
    }
  }

---

import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
  import type {                                                                                                                                                                           
    AnyAgentTool,                                                                                                                                                                         
    OpenClawPluginApi,                                                                                                                                                                    
    OpenClawPluginToolFactory,                                                                                                                                                            
  } from "openclaw/plugin-sdk/plugin-entry";                                                                                                                                              
                                                                                                                                                                                        
  export default definePluginEntry({                                                                                                                                                      
    id: "dummy-test",                                         
    name: "Dummy Test",                                                                                                                                                                   
    description: "Minimal A/B test plugin",
                                                                                                                                                                                          
    register(api: OpenClawPluginApi) {                        
      api.logger?.info("[dummy-test:DIAG] register() ENTERED");                                                                                                                           
                                                                                                                                                                                          
      api.registerTool(                                                                                                                                                                   
        ((ctx) => {                                                                                                                                                                       
          api.logger?.info(                                                                                                                                                               
            `[dummy-test:DIAG] FACTORY called sandboxed=${ctx.sandboxed}`,                                                                                                              
          );                                                                                                                                                                              
          if (ctx.sandboxed) return null;                     
          const tool: AnyAgentTool = {                                                                                                                                                    
            name: "dummy-test",                               
            label: "Dummy Test",                                                                                                                                                          
            description: "Returns 'pong' to verify tool exposure.",                                                                                                                       
            parameters: { type: "object", properties: {}, additionalProperties: false },                                                                                                  
            async execute() {                                                                                                                                                             
              return {                                                                                                                                                                    
                content: [{ type: "text" as const, text: "pong" }],                                                                                                                     
                details: { ok: true },                                                                                                                                                    
              };                                                                                                                                                                          
            },                                                                                                                                                                          
          } as unknown as AnyAgentTool;                                                                                                                                                   
          return tool;                                        
        }) as OpenClawPluginToolFactory,                                                                                                                                                
        { optional: true },
      );                                                                                                                                                                                  
   
      api.logger?.info("[dummy-test:DIAG] register() EXITED");                                                                                                                            
    },                                                        
  });

---

{
    "plugins": {
      "allow": ["dummy-test"],
      "load": { "paths": ["/path/to/dummy-test"] },                                                                                                                                       
      "entries": { "dummy-test": { "enabled": true } }
    },                                                                                                                                                                                    
    "tools": { "allow": ["dummy-test", "group:plugins"] }     
  }

---

[dummy-test:DIAG] register() ENTERED                        
  [dummy-test:DIAG] register() EXITED                                                                                                                                                   
  [dummy-test:DIAG] FACTORY called sandboxed=false  ← appears at first agent request

---

[dummy-test:DIAG] register() ENTERED                        
  [dummy-test:DIAG] register() EXITED                                                                                                                                                   
  # ... agent requests come and go, FACTORY never called ...

---

function resolvePluginToolRegistry(params) {                
    // 1. Try channel registry (if gateway-bindable / pinned)                                                                                                                           
    // 2. Try active registry                                                                                                                                                             
    // 3. FALLBACK: load on demand via jiti                                                                                                                                               
    return resolveRuntimePluginRegistry(params.loadOptions);  // ← removed                                                                                                                
  }

---

function resolvePluginToolRegistry(params) {
    return (                                                                                                                                                                              
      getLoadedRuntimePluginRegistry({ ..., surface: "channel" }) ??
      getLoadedRuntimePluginRegistry({ ..., surface: "active" })                                                                                                                          
    );                                                                                                                                                                                    
    // ← no fallback; returns undefined if neither has the plugin                                                                                                                         
  }

---

const registry = resolvePluginToolRegistry({ loadOptions, onlyPluginIds: runtimePluginIds });
  if (!registry) {                                                                                                                                                                        
    return tools;  // ← bails silently, factory never invoked
  }

---
RAW_BUFFERClick to expand / collapse

Bug type

Regression (worked before, now fails)

Beta release blocker

No

Summary

Summary

After upgrading from v2026.4.29 to v2026.5.2, agent tools registered by path-based plugins (loaded via plugins.load.paths, manifest origin "config") silently fail to appear in agent tool palettes. The plugin's register() callback is invoked at gateway startup, but the tool factory is never called when an agent resolves its tool list, so the gateway returns unknown method errors when the agent tries to call the tool.

This is the same regression family that PR #76536 fixes for capability providers and PR #76393 fixed for memory-plugin doctor/status — the on-demand load fallback that PR #76004
("perf(plugins): reuse startup runtime registry") removed from resolvePluginToolRegistry in src/plugins/tools.ts is also needed for tool resolution.

The Codex review on PR #76004 explicitly flagged this exact risk before merge:

"Keep plugin tools from disappearing on cold registries — resolvePluginTools now only reads the loaded channel registry."

Steps to reproduce

Symptom

Gateway logs:

error Gateway call failed: GatewayClientRequestError: unknown method: <tool-name>.run
info gateway/ws ⇄ res ✗ <tool-name>.run 0ms errorCode=INVALID_REQUEST errorMessage=unknown method: <tool-name>.run

The agent thinks the tool isn't registered and skips/fails the call. No warning, no diagnostic — silent failure.

Minimal reproducer

A 40-line plugin that demonstrates the bug deterministically. Drop into a directory listed under plugins.load.paths, allowlist dummy-test in your config, restart the gateway, and
ask any agent to invoke the tool.

dummy-test/package.json:

{                                                           
  "name": "@example/dummy-test",                                                                                                                                                      
  "version": "0.1.0",
  "type": "module",
  "openclaw": {
    "extensions": ["./index.ts"],
    "compat": { "pluginApi": ">=2026.3.24-beta.2" }                                                                                                                                     
  }                                                                                                                                                                                     
}

dummy-test/openclaw.plugin.json:

{
  "id": "dummy-test",
  "name": "Dummy Test",
  "description": "Minimal plugin to verify path-based tool plugins work.",                                                                                                              
  "activation": { "onStartup": true },
  "contracts": { "tools": ["dummy-test"] },                                                                                                                                             
  "configSchema": {                                         
    "type": "object",                                                                                                                                                                   
    "additionalProperties": false,                          
    "properties": {}                                                                                                                                                                  
  }
}

dummy-test/index.ts:

import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import type {                                                                                                                                                                           
  AnyAgentTool,                                                                                                                                                                         
  OpenClawPluginApi,                                                                                                                                                                    
  OpenClawPluginToolFactory,                                                                                                                                                            
} from "openclaw/plugin-sdk/plugin-entry";                                                                                                                                              
                                                                                                                                                                                      
export default definePluginEntry({                                                                                                                                                      
  id: "dummy-test",                                         
  name: "Dummy Test",                                                                                                                                                                   
  description: "Minimal A/B test plugin",
                                                                                                                                                                                        
  register(api: OpenClawPluginApi) {                        
    api.logger?.info("[dummy-test:DIAG] register() ENTERED");                                                                                                                           
                                                                                                                                                                                        
    api.registerTool(                                                                                                                                                                   
      ((ctx) => {                                                                                                                                                                       
        api.logger?.info(                                                                                                                                                               
          `[dummy-test:DIAG] FACTORY called sandboxed=${ctx.sandboxed}`,                                                                                                              
        );                                                                                                                                                                              
        if (ctx.sandboxed) return null;                     
        const tool: AnyAgentTool = {                                                                                                                                                    
          name: "dummy-test",                               
          label: "Dummy Test",                                                                                                                                                          
          description: "Returns 'pong' to verify tool exposure.",                                                                                                                       
          parameters: { type: "object", properties: {}, additionalProperties: false },                                                                                                  
          async execute() {                                                                                                                                                             
            return {                                                                                                                                                                    
              content: [{ type: "text" as const, text: "pong" }],                                                                                                                     
              details: { ok: true },                                                                                                                                                    
            };                                                                                                                                                                          
          },                                                                                                                                                                          
        } as unknown as AnyAgentTool;                                                                                                                                                   
        return tool;                                        
      }) as OpenClawPluginToolFactory,                                                                                                                                                
      { optional: true },
    );                                                                                                                                                                                  
 
    api.logger?.info("[dummy-test:DIAG] register() EXITED");                                                                                                                            
  },                                                        
});

openclaw.json (relevant snippets):

{
  "plugins": {
    "allow": ["dummy-test"],
    "load": { "paths": ["/path/to/dummy-test"] },                                                                                                                                       
    "entries": { "dummy-test": { "enabled": true } }
  },                                                                                                                                                                                    
  "tools": { "allow": ["dummy-test", "group:plugins"] }     
}

Expected (works on v2026.4.29)

Gateway log shows both DIAG lines on startup AND on first agent request:

[dummy-test:DIAG] register() ENTERED                        
[dummy-test:DIAG] register() EXITED                                                                                                                                                   
[dummy-test:DIAG] FACTORY called sandboxed=false  ← appears at first agent request

Agent invocation of dummy-test returns "pong".

Actual (broken on v2026.5.2)

Gateway log shows only the register() lines on startup. The FACTORY called line never appears, no matter how many agent requests are made:

[dummy-test:DIAG] register() ENTERED                        
[dummy-test:DIAG] register() EXITED                                                                                                                                                   
# ... agent requests come and go, FACTORY never called ...

Agent invocation fails with unknown method: dummy-test.run.

Expected behavior

Expected (works on v2026.4.29)

Gateway log shows both DIAG lines on startup AND on first agent request:

[dummy-test:DIAG] register() ENTERED                        
[dummy-test:DIAG] register() EXITED                                                                                                                                                   
[dummy-test:DIAG] FACTORY called sandboxed=false  ← appears at first agent request

Agent invocation of dummy-test returns "pong".

Actual behavior

Summary

After upgrading from v2026.4.29 to v2026.5.2, agent tools registered by path-based plugins (loaded via plugins.load.paths, manifest origin "config") silently fail to appear in agent tool palettes. The plugin's register() callback is invoked at gateway startup, but the tool factory is never called when an agent resolves its tool list, so the gateway returns unknown method errors when the agent tries to call the tool.

This is the same regression family that PR #76536 fixes for capability providers and PR #76393 fixed for memory-plugin doctor/status — the on-demand load fallback that PR #76004
("perf(plugins): reuse startup runtime registry") removed from resolvePluginToolRegistry in src/plugins/tools.ts is also needed for tool resolution.

The Codex review on PR #76004 explicitly flagged this exact risk before merge:

"Keep plugin tools from disappearing on cold registries — resolvePluginTools now only reads the loaded channel registry."

This issue is a real-world repro of that warning.

Affected versions

  • Broken: v2026.5.2-beta.2, v2026.5.2
  • Working: v2026.4.29 (rollback fixes the issue immediately)

Environment

  • Node.js 22.x
  • Linux (Ubuntu) — gateway runs as a systemd service
  • Plugin loaded via plugins.load.paths
  • Plugin manifest declares activation.onStartup: true and contracts.tools: ["<tool-name>"]
  • Plugin allowlisted in openclaw.json (both plugins.allow and tools.allow and agent.tools.allow)

Symptom

Gateway logs:

error Gateway call failed: GatewayClientRequestError: unknown method: <tool-name>.run
info gateway/ws ⇄ res ✗ <tool-name>.run 0ms errorCode=INVALID_REQUEST errorMessage=unknown method: <tool-name>.run

The agent thinks the tool isn't registered and skips/fails the call. No warning, no diagnostic — silent failure.

Minimal reproducer

A 40-line plugin that demonstrates the bug deterministically. Drop into a directory listed under plugins.load.paths, allowlist dummy-test in your config, restart the gateway, and
ask any agent to invoke the tool.

dummy-test/package.json:

{                                                           
  "name": "@example/dummy-test",                                                                                                                                                      
  "version": "0.1.0",
  "type": "module",
  "openclaw": {
    "extensions": ["./index.ts"],
    "compat": { "pluginApi": ">=2026.3.24-beta.2" }                                                                                                                                     
  }                                                                                                                                                                                     
}

dummy-test/openclaw.plugin.json:

{
  "id": "dummy-test",
  "name": "Dummy Test",
  "description": "Minimal plugin to verify path-based tool plugins work.",                                                                                                              
  "activation": { "onStartup": true },
  "contracts": { "tools": ["dummy-test"] },                                                                                                                                             
  "configSchema": {                                         
    "type": "object",                                                                                                                                                                   
    "additionalProperties": false,                          
    "properties": {}                                                                                                                                                                  
  }
}

dummy-test/index.ts:

import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import type {                                                                                                                                                                           
  AnyAgentTool,                                                                                                                                                                         
  OpenClawPluginApi,                                                                                                                                                                    
  OpenClawPluginToolFactory,                                                                                                                                                            
} from "openclaw/plugin-sdk/plugin-entry";                                                                                                                                              
                                                                                                                                                                                      
export default definePluginEntry({                                                                                                                                                      
  id: "dummy-test",                                         
  name: "Dummy Test",                                                                                                                                                                   
  description: "Minimal A/B test plugin",
                                                                                                                                                                                        
  register(api: OpenClawPluginApi) {                        
    api.logger?.info("[dummy-test:DIAG] register() ENTERED");                                                                                                                           
                                                                                                                                                                                        
    api.registerTool(                                                                                                                                                                   
      ((ctx) => {                                                                                                                                                                       
        api.logger?.info(                                                                                                                                                               
          `[dummy-test:DIAG] FACTORY called sandboxed=${ctx.sandboxed}`,                                                                                                              
        );                                                                                                                                                                              
        if (ctx.sandboxed) return null;                     
        const tool: AnyAgentTool = {                                                                                                                                                    
          name: "dummy-test",                               
          label: "Dummy Test",                                                                                                                                                          
          description: "Returns 'pong' to verify tool exposure.",                                                                                                                       
          parameters: { type: "object", properties: {}, additionalProperties: false },                                                                                                  
          async execute() {                                                                                                                                                             
            return {                                                                                                                                                                    
              content: [{ type: "text" as const, text: "pong" }],                                                                                                                     
              details: { ok: true },                                                                                                                                                    
            };                                                                                                                                                                          
          },                                                                                                                                                                          
        } as unknown as AnyAgentTool;                                                                                                                                                   
        return tool;                                        
      }) as OpenClawPluginToolFactory,                                                                                                                                                
      { optional: true },
    );                                                                                                                                                                                  
 
    api.logger?.info("[dummy-test:DIAG] register() EXITED");                                                                                                                            
  },                                                        
});

openclaw.json (relevant snippets):

{
  "plugins": {
    "allow": ["dummy-test"],
    "load": { "paths": ["/path/to/dummy-test"] },                                                                                                                                       
    "entries": { "dummy-test": { "enabled": true } }
  },                                                                                                                                                                                    
  "tools": { "allow": ["dummy-test", "group:plugins"] }     
}

Expected (works on v2026.4.29)

Gateway log shows both DIAG lines on startup AND on first agent request:

[dummy-test:DIAG] register() ENTERED                        
[dummy-test:DIAG] register() EXITED                                                                                                                                                   
[dummy-test:DIAG] FACTORY called sandboxed=false  ← appears at first agent request

Agent invocation of dummy-test returns "pong".

Actual (broken on v2026.5.2)

Gateway log shows only the register() lines on startup. The FACTORY called line never appears, no matter how many agent requests are made:

[dummy-test:DIAG] register() ENTERED                        
[dummy-test:DIAG] register() EXITED                                                                                                                                                   
# ... agent requests come and go, FACTORY never called ...

Agent invocation fails with unknown method: dummy-test.run.

Root cause

PR #76004 (commit 8283c5d6cc, "perf(plugins): reuse startup runtime registry") rewrote resolvePluginToolRegistry in src/plugins/tools.ts.

Before (v2026.4.29):

function resolvePluginToolRegistry(params) {                
  // 1. Try channel registry (if gateway-bindable / pinned)                                                                                                                           
  // 2. Try active registry                                                                                                                                                             
  // 3. FALLBACK: load on demand via jiti                                                                                                                                               
  return resolveRuntimePluginRegistry(params.loadOptions);  // ← removed                                                                                                                
}

After (v2026.5.2):

function resolvePluginToolRegistry(params) {
  return (                                                                                                                                                                              
    getLoadedRuntimePluginRegistry({ ..., surface: "channel" }) ??
    getLoadedRuntimePluginRegistry({ ..., surface: "active" })                                                                                                                          
  );                                                                                                                                                                                    
  // ← no fallback; returns undefined if neither has the plugin                                                                                                                         
}

And in resolvePluginTools:

const registry = resolvePluginToolRegistry({ loadOptions, onlyPluginIds: runtimePluginIds });
if (!registry) {                                                                                                                                                                        
  return tools;  // ← bails silently, factory never invoked
}

Why bundled plugins still work: Bundled extensions (lobster, llm-task, memory-core) are loaded into the channel registry at gateway startup via bundled-channel-runtime, so they're always present when surface: "channel" is queried.

Why path-based plugins don't: Plugins loaded via plugins.load.paths (manifest origin: "config") have their register() callback invoked during a separate discovery pass that
doesn't promote them to the channel registry with status: "loaded". The pre-2026.5.2 fallback resolveRuntimePluginRegistry(params.loadOptions) would load them on demand. With the fallback removed, they're invisible to resolvePluginTools.

Same regression family — already partially fixed

The maintainers have already acknowledged and fixed this exact pattern in two adjacent surfaces:

  • PR #76536 "fix(plugins): preserve external capability provider fallback" (open, ready to merge): patches capability-provider-runtime.ts. Description quotes verbatim:

    "preserve the v2026.5.2 fast path that reuses an already-loaded runtime registry, but fall back to the scoped manifest-derived runtime load when that registry is missing the provider."

  • PR #76393 "fix(cli): load memory plugin for doctor/status when registry is cold" (merged): patches the doctor/status command path with the same scoped fallback.

Neither patches src/plugins/tools.ts, so the tool resolution surface — used by every agent on every turn — is still broken.

Likely-related issues that may be the same bug from different angles:

  • #76533 (active-memory tools missing after 2026.5.2) — auto-closed by clawsweeper[bot] referencing an unrelated commit; probably should be reopened.
  • #76576 (context-engine plugin loaded but not registered) — same symptom pattern (loaded ≠ registered) for the context-engine slot.
  • #76507 (browser tool missing with tools.profile: full) — different code path (allowlist filtering), but confirms multiple tool-resolution regressions in 2026.5.2.

Workaround (in production today)

Pin to v2026.4.29. Patching the installed dist/plugins/tools.js with the diff above also works as a stopgap (verified locally — FACTORY called appears, agent returns "pong").

Acceptance / regression coverage

A regression test should ensure that a path-based plugin (origin "config") with a registered tool factory has that factory invoked when an agent resolves its tool list — even when
the channel and active registries are cold for that plugin id. The dummy-test plugin above can serve as the integration fixture.

OpenClaw version

2026.05.02

Operating system

Linux (Ubuntu)

Install method

No response

Model

gemini

Provider / routing chain

openclaw

Additional provider/model setup details

No response

Logs, screenshots, and evidence

Impact and severity

No response

Additional information

No response

extent analysis

TL;DR

The issue can be fixed by reverting the change made in PR #76004 or by adding a fallback to load plugins on demand in resolvePluginToolRegistry.

Guidance

  • The root cause of the issue is the removal of the fallback to load plugins on demand in resolvePluginToolRegistry in src/plugins/tools.ts.
  • To fix the issue, you can either revert the change made in PR #76004 or add a fallback to load plugins on demand.
  • You can also use the workaround of pinning to v2026.4.29 or patching the installed dist/plugins/tools.js with the diff.
  • A regression test should be added to ensure that a path-based plugin with a registered tool factory has that factory invoked when an agent resolves its tool list.

Example

function resolvePluginToolRegistry(params) {
  // 1. Try channel registry (if gateway-bindable / pinned)
  // 2. Try active registry
  // 3. FALLBACK: load on demand via jiti
  return resolveRuntimePluginRegistry(params.loadOptions);
}

Notes

  • The issue is specific to path-based plugins loaded via plugins.load.paths with manifest origin "config".
  • The change made in PR #76004 removed the fallback to load plugins on demand, which caused the issue.
  • The workaround of pinning to v2026.4.29 or patching the installed dist/plugins/tools.js is temporary and should be replaced with a proper fix.

Recommendation

Apply the workaround of pinning to v2026.4.29 until a proper fix is available. This will ensure that the plugin tools are loaded correctly and the agent can invoke them without errors.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

Expected (works on v2026.4.29)

Gateway log shows both DIAG lines on startup AND on first agent request:

[dummy-test:DIAG] register() ENTERED                        
[dummy-test:DIAG] register() EXITED                                                                                                                                                   
[dummy-test:DIAG] FACTORY called sandboxed=false  ← appears at first agent request

Agent invocation of dummy-test returns "pong".

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING