Skip to content

[Bug]: Path-based plugin tools (origin "config") not exposed after 2026.5.2 #76598

@minhhoangvn

Description

@minhhoangvn

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:

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:

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingregressionBehavior that previously worked and now fails

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions