openclaw - 💡(How to fix) Fix Optional plugin tools missing from agent runtime inventory despite all static checks passing

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…

Error Message

Why I'm confident this isn't a config error

Fix Action

Fix / Workaround

// index.ts export default definePluginEntry({ id: "x-tool-plugin", register(api) { api.registerTool( () => ({ name: "x_tool", description: "Returns 'ok'", parameters: Type.Object({}), async execute() { return { content: [{ type: "text", text: "ok" }] }; }, }), { name: "x_tool", optional: true }, ); }, });

2. `openclaw plugins install <path> --force` then restart the gateway.
3. Configure an agent (`agents.list[i]` for any agent like `forge`) with a restrictive `tools.allow` that includes `"x_tool"`:
```json
{
  "id": "forge",
  "tools": {
    "allow": ["read","write","edit","web_search","web_fetch","message","sessions_spawn","sessions_yield","subagents","x_tool"],
    "deny":  ["apply_patch","exec","process","browser","canvas","cron","gateway","agents_list","sessions_list","sessions_history","sessions_send"],
    "fs":    { "workspaceOnly": true }
  },
  "subagents": { "allowAgents": ["research","deep-analyst","security-agent"] }
}
  1. Add an openclaw.json plugin entry: plugins.entries.x-tool-plugin = { enabled: true, config: { ... } }. Add plugins.allow: ["x-tool-plugin"].
  2. Verify static layers (all should succeed):
    openclaw plugins inspect x-tool-plugin --runtime --json | jq '.plugin.toolNames'
    # → ["x_tool"]
  3. From a Node script, call resolveEffectiveToolInventory with sessionKey: "agent:forge:telegram:direct:<userId>". The result lists x_tool in the plugin group.
  4. Send the agent a Telegram message asking "list your available tools". The agent reports 8 tools without x_tool and without message:
    read, write, edit, web_search, web_fetch, sessions_spawn, sessions_yield, subagents

High for plugin authors. There is no static configuration that surfaces an optional plugin tool to an agent with a restrictive tools.allow running on the openclaw harness over Telegram. The only working path I found is to drop the agent's tools.allow entirely (which removes all per-agent restriction). That's not an acceptable workaround for security-sensitive plugins.

Code Example

// openclaw.plugin.json
   {
     "id": "x-tool-plugin",
     "activation": { "onStartup": true },
     "enabledByDefault": false,
     "contracts": { "tools": ["x_tool"] },
     "toolMetadata": { "x_tool": { "optional": true } }
   }

   // index.ts
   export default definePluginEntry({
     id: "x-tool-plugin",
     register(api) {
       api.registerTool(
         () => ({
           name: "x_tool",
           description: "Returns 'ok'",
           parameters: Type.Object({}),
           async execute() { return { content: [{ type: "text", text: "ok" }] }; },
         }),
         { name: "x_tool", optional: true },
       );
     },
   });

---

{
     "id": "forge",
     "tools": {
       "allow": ["read","write","edit","web_search","web_fetch","message","sessions_spawn","sessions_yield","subagents","x_tool"],
       "deny":  ["apply_patch","exec","process","browser","canvas","cron","gateway","agents_list","sessions_list","sessions_history","sessions_send"],
       "fs":    { "workspaceOnly": true }
     },
     "subagents": { "allowAgents": ["research","deep-analyst","security-agent"] }
   }

---

openclaw plugins inspect x-tool-plugin --runtime --json | jq '.plugin.toolNames'
   # → ["x_tool"]

---

read, write, edit, web_search, web_fetch, sessions_spawn, sessions_yield, subagents

---

RegistrySnapshot.plugins.entries[gmail-monitor].toolNames = ["forge_gmail"]   # snapshot says yes
admin: resolveEffectiveToolInventory(sessionKey).groups[plugin].tools         # admin says yes
admin: createOpenClawCodingTools({…sessionKey…}) returns                      # CLI invocation says yes
runtime: prompting.systemPromptReport.tools.entries                            # live run says NO
runtime: model receives 8 tools, no plugin tools, no message tool             # live run says NO

---

const includePluginTools = normalized.some(name =>
  name === "group:plugins" ||
  (!isBundleMcpAllowlistName(name) && !isKnownLocalCodingToolName(name)));

---

test("agent.tools.allow with plugin tool surfaces it at runtime", async () => {
  // load minimal plugin with optional tool 'x_tool'
  // configure agent with tools.allow: ['read', 'x_tool']
  // resolve agent's runtime tool list
  // expect 'x_tool' to be present
});
RAW_BUFFERClick to expand / collapse

Optional plugin tools missing from agent runtime inventory despite all static checks passing

TL;DR

A non-bundled plugin with activation.onStartup: true registers an optional tool. Every static layer confirms the tool is registered, allowed, and resolvable for a given agent session. The same tool is silently absent from that agent's actual runtime tool inventory when the agent serves a Telegram-routed turn. The message core channel tool disappears alongside the plugin tool. Effect is reproducible across 3 separate plugins (file-transfer, memory-core, and a private gmail-monitor wrapper) — only their tools are missing; their plugins load fine.

This blocks plugin authors from delivering optional tools to agents with restrictive per-agent tools.allow.

Environment

  • Version: 2026.5.6-beta.1 (8e9c8be)
  • OS: macOS 26.3.1 (arm64), Node v25.5.0
  • Harness: type: "openclaw" (native, not codex extension)
  • Provider/model: openai-codex / gpt-5.5 via openai-codex-responses
  • Channel: Telegram, both DM and group chat sessions
  • Gateway invocation: node openclaw/dist/index.js gateway --port <port>

Repro (clean install path)

  1. Build a minimal plugin using definePluginEntry:
    // openclaw.plugin.json
    {
      "id": "x-tool-plugin",
      "activation": { "onStartup": true },
      "enabledByDefault": false,
      "contracts": { "tools": ["x_tool"] },
      "toolMetadata": { "x_tool": { "optional": true } }
    }
    
    // index.ts
    export default definePluginEntry({
      id: "x-tool-plugin",
      register(api) {
        api.registerTool(
          () => ({
            name: "x_tool",
            description: "Returns 'ok'",
            parameters: Type.Object({}),
            async execute() { return { content: [{ type: "text", text: "ok" }] }; },
          }),
          { name: "x_tool", optional: true },
        );
      },
    });
  2. openclaw plugins install <path> --force then restart the gateway.
  3. Configure an agent (agents.list[i] for any agent like forge) with a restrictive tools.allow that includes "x_tool":
    {
      "id": "forge",
      "tools": {
        "allow": ["read","write","edit","web_search","web_fetch","message","sessions_spawn","sessions_yield","subagents","x_tool"],
        "deny":  ["apply_patch","exec","process","browser","canvas","cron","gateway","agents_list","sessions_list","sessions_history","sessions_send"],
        "fs":    { "workspaceOnly": true }
      },
      "subagents": { "allowAgents": ["research","deep-analyst","security-agent"] }
    }
  4. Add an openclaw.json plugin entry: plugins.entries.x-tool-plugin = { enabled: true, config: { ... } }. Add plugins.allow: ["x-tool-plugin"].
  5. Verify static layers (all should succeed):
    openclaw plugins inspect x-tool-plugin --runtime --json | jq '.plugin.toolNames'
    # → ["x_tool"]
  6. From a Node script, call resolveEffectiveToolInventory with sessionKey: "agent:forge:telegram:direct:<userId>". The result lists x_tool in the plugin group.
  7. Send the agent a Telegram message asking "list your available tools". The agent reports 8 tools without x_tool and without message:
    read, write, edit, web_search, web_fetch, sessions_spawn, sessions_yield, subagents

Expected vs actual

Expected: the agent's runtime tool inventory should include the same tools that resolveEffectiveToolInventory and createOpenClawCodingTools return for the same session key. With the configuration above, the agent should see at minimum: [..., message, x_tool] in addition to the 8 core tools listed.

Actual:

  • Static admin-API path → ✅ tool present
  • --runtime plugin inspect → ✅ tool present (toolNames: ["x_tool"])
  • Direct invocation createOpenClawCodingTools({ ..., sessionKey: "agent:forge:telegram:direct:<id>" }) from a separate Node process → ✅ returns 10 tools including x_tool and message
  • Same agent's actual Telegram-routed run → ❌ 8 tools, no x_tool, no message
  • Trajectory prompt.submitted + context.compiled.tools → 8 tools (no plugin tools, no channel tools)
  • Trajectory prompting.systemPromptReport.tools.entries → 8 entries (matches above)
  • Trajectory plugins.entries[<i>].toolNames["x_tool"] (proves plugin loaded fine)

Diagnostic divergence (the smoking gun)

RegistrySnapshot.plugins.entries[gmail-monitor].toolNames = ["forge_gmail"]   # snapshot says yes
admin: resolveEffectiveToolInventory(sessionKey).groups[plugin].tools         # admin says yes
admin: createOpenClawCodingTools({…sessionKey…}) returns                      # CLI invocation says yes
runtime: prompting.systemPromptReport.tools.entries                            # live run says NO
runtime: model receives 8 tools, no plugin tools, no message tool             # live run says NO

The same code path (createOpenClawCodingTools) returns different tool sets depending on whether it's called from the live gateway turn or a separate Node process loading the same dist files with the same config — strongly suggesting the live runtime takes a code branch the CLI does not, OR a per-session/per-turn cache holds a stale snapshot taken before plugin runtime activation completed.

Symptom cluster

The same 3 enabled non-bundled plugins are all affected:

PluginTools registered (per plugins.entries[].toolNames)Tools surfaced to FORGE
file-transferdir_fetch, dir_list, file_fetch, file_writenone
memory-corememory_get, memory_searchnone
gmail-monitor (private)forge_gmailnone

The message core channel tool also disappears alongside plugin tools — strong hint that whatever filter is responsible treats message and plugin tools the same way (likely disableMessageTool and disablePluginTools both true, or the construction plan resolves to includeChannelTools: false and includePluginTools: false for this session class).

Code pointers (file-locations of the suspected gate)

  • dist/pi-tools-DepR0oGW.js:
    • createOpenClawCodingTools() — the compositor; pi-tools.ts source
    • The branching ...includeOpenClawTools ? createOpenClawTools({...disablePluginTools: !includePluginTools...}) : pluginToolsOnly
    • pluginToolsOnly = includeOpenClawTools || !includePluginTools ? [] : resolveOpenClawPluginToolsForOptions(...)
  • dist/selection-BeP8qtCb.js:
    • resolveEmbeddedAttemptToolConstructionPlan(params) — derives toolConstructionPlan from params.toolsAllow
    • resolveCodingToolConstructionPlanForAllowlist(toolsAllow) — sets includeChannelTools and includePluginTools
    • The toolsRaw = !toolConstructionPlan.constructTools ? [] : (() => createOpenClawCodingTools({...buildEmbeddedAttemptToolRunContext(params)...}))() branch
  • dist/openclaw-tools-0ftkmYS3.js:
    • createOpenClawTools() — gates message tool with options?.disableMessageTool and plugin tools with options?.disablePluginTools

Suspected mechanism (best guess for fix shape)

resolveCodingToolConstructionPlanForAllowlist(toolsAllow) may be invoked with toolsAllow = agent.tools.allow (the agent's own per-agent allowlist) rather than the runtime's params.toolsAllow (which is for cron/cli toolsAllow overrides). The check

const includePluginTools = normalized.some(name =>
  name === "group:plugins" ||
  (!isBundleMcpAllowlistName(name) && !isKnownLocalCodingToolName(name)));

is intended to detect when the run's toolsAllow includes any plugin tool. But if this gets fed the agent's static allowlist (which lists tools by literal name), and isKnownLocalCodingToolName accidentally returns true for entries like "sessions_spawn"/"subagents", the predicate may never see a "plugin" entry, returning includePluginTools: false.

Mirror question for includeChannelTools and message — same tools.allow array, similar predicate.

A maintainer can verify by adding console.log({toolsAllow, includePluginTools, includeChannelTools, includeOpenClawTools, includeBaseCodingTools}) at the start of resolveCodingToolConstructionPlanForAllowlist and re-running the repro.

Why I'm confident this isn't a config error

I attempted every single per-agent and global remediation listed in the docs and source comments. None changed the runtime outcome:

  • agents.list[forge].tools.allow: literal "x_tool"
  • agents.list[forge].tools.allow: plugin id "x-tool-plugin"
  • agents.list[forge].tools.allow: "group:plugins"
  • agents.list[forge].tools.allow: "__openclaw_default_plugin_tools__"
  • agents.list[forge].tools.profile: "full"
  • plugins.entries.x-tool-plugin.enabled: true
  • plugins.entries.x-tool-plugin.config.enabled: true
  • plugins.allow: ["x-tool-plugin"]
  • plugins.entries.x-tool-plugin.config.allowedSessionKeyPrefixes: includes "agent:forge:telegram:"
  • Manifest activation.onStartup: true
  • Gateway full restart via launchctl kickstart -k after each change ✅
  • /new slash command for fresh session per attempt ✅

openclaw plugins doctor reports "No plugin issues detected" throughout, and openclaw plugins inspect <id> --runtime --json always shows the tool in toolNames.

Severity

High for plugin authors. There is no static configuration that surfaces an optional plugin tool to an agent with a restrictive tools.allow running on the openclaw harness over Telegram. The only working path I found is to drop the agent's tools.allow entirely (which removes all per-agent restriction). That's not an acceptable workaround for security-sensitive plugins.

Useful artifacts (privately reproducible)

  • Plugin source for repro available on request (@bobproai/openclaw-gmail-monitor-plugin, MIT-style).
  • Trajectory file showing the divergence: ~/.openclaw/agents/forge/sessions/<sessionId>.trajectory.jsonl — contains both plugins.entries[].toolNames: ["forge_gmail"] and prompting.systemPromptReport.tools.entries of length 8.
  • Scripted reproducer that loads pi-tools-DepR0oGW.js and calls createOpenClawCodingTools directly with FORGE's session key — returns the tool, confirming the divergence is at runtime, not in code.

I'm happy to provide a sanitized full repro tarball, the trajectory excerpts, and the gateway log windows showing plugin load + runtime tool list — just say what format you'd like.

Suggested fix shape

  1. Identify whether resolveCodingToolConstructionPlanForAllowlist is being called with the wrong toolsAllow (per-agent static vs per-run runtime). If yes — pass undefined here so the default ALL_CODING_TOOL_CONSTRUCTION_PLAN is used and the per-agent restriction lives only in the policy-pipeline filter step (where it currently does correctly include plugin tools).
  2. Or, fix includePluginTools predicate so that an agent.tools.allow containing literal plugin tool names is recognized as a plugin-tool intent.
  3. Either fix should make the repro emit 10 tools instead of 8 to the model, with forge_gmail and message included.

A targeted regression test:

test("agent.tools.allow with plugin tool surfaces it at runtime", async () => {
  // load minimal plugin with optional tool 'x_tool'
  // configure agent with tools.allow: ['read', 'x_tool']
  // resolve agent's runtime tool list
  // expect 'x_tool' to be present
});

Happy to send a PR for the test and the fix once the maintainer confirms direction.

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…

Still need to ship something?

×6

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

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix Optional plugin tools missing from agent runtime inventory despite all static checks passing