Skip to content

Bug: disabled bundled tool plugins can drop all plugin-backed tools from agent surface #76603

Description

@jwong-art

Summary

After upgrading to OpenClaw 2026.5.2 (8b2a6e5), enabled plugin-backed tools such as browser, memory_search, and memory_get can disappear from the effective agent tool surface, even when:

  • plugins.entries.browser.enabled: true
  • plugins.entries.memory-core.enabled: true
  • browser.enabled: true
  • agents.defaults.memorySearch.enabled: true
  • tools.profile: "full" or the agent-level profile is "full"

The issue is not caused by the browser or memory plugins being disabled, nor by the full tool profile filtering them out. The failure appears to be in the plugin tool resolution/registry filtering path: disabled bundled plugin IDs are included in the required plugin ID set for tool discovery, and then getLoadedRuntimePluginRegistry() refuses to return the registry because those disabled plugins are not considered loaded. As a result, resolvePluginTools() returns no plugin tools at all, which drops unrelated enabled plugin tools (browser, memory_search, memory_get).

Expected behavior

If browser and memory-core are enabled and pass availability checks, their tools should be present in the effective tool list:

  • browser
  • memory_search
  • memory_get

Disabled unrelated bundled plugins with tool contracts (for example firecrawl, tavily, etc.) should not cause all plugin tools to be dropped.

Actual behavior

The effective tool list contains built-in/core tools such as read, exec, web_search, web_fetch, canvas, message, etc., but does not contain:

  • browser
  • memory_search
  • memory_get

A local effective inventory check showed the active tool entries included only core tools; the three plugin tools above were absent.

Sanitized configuration shape

The configuration has the relevant plugin/tool settings enabled. Sensitive/local values omitted:

{
  "tools": {
    "profile": "full"
  },
  "browser": {
    "enabled": true,
    "evaluateEnabled": true,
    "headless": true,
    "defaultProfile": "openclaw"
  },
  "agents": {
    "defaults": {
      "memorySearch": {
        "enabled": true,
        "provider": "ollama",
        "model": "nomic-embed-text:latest",
        "fallback": "none"
      }
    },
    "list": [
      {
        "tools": {
          "profile": "full"
        }
      }
    ]
  },
  "plugins": {
    "entries": {
      "browser": { "enabled": true },
      "memory-core": { "enabled": true }
    }
  }
}

openclaw plugins list also reports both bundled plugins enabled:

  • browser enabled
  • memory-core enabled

openclaw plugins doctor only reported an unrelated third-party/local plugin warning:

Diagnostics:
- minimax-web-search: plugin must declare contracts.tools before registering agent tools

That diagnostic is unrelated to browser and memory-core; it does not explain why all plugin tools are dropped.

Investigation details / suspected root cause

1. browser and memory tools are plugin-backed, not core-created in createOpenClawTools()

/usr/lib/node_modules/openclaw/dist/openclaw-tools-BDF6gNXk.js

createOpenClawTools() builds core tools first, then appends plugin tools:

const wrappedPluginTools = resolveOpenClawPluginToolsForOptions({
  options,
  resolvedConfig,
  existingToolNames: new Set(tools.map((tool) => tool.name))
});
return [...tools, ...wrappedPluginTools];

The browser and memory tools come from plugin manifests/runtime registration:

/usr/lib/node_modules/openclaw/dist/extensions/browser/openclaw.plugin.json

{
  "id": "browser",
  "contracts": {
    "tools": ["browser"]
  }
}

/usr/lib/node_modules/openclaw/dist/extensions/memory-core/openclaw.plugin.json

{
  "id": "memory-core",
  "contracts": {
    "tools": ["memory_get", "memory_search"]
  }
}

Runtime registration also exists:

/usr/lib/node_modules/openclaw/dist/plugin-registration-DiVrZUS2.js

function registerBrowserPlugin(api) {
  api.registerTool(((ctx) => createLazyBrowserTool(...)));
}

/usr/lib/node_modules/openclaw/dist/extensions/memory-core/index.js

api.registerTool((ctx) => createLazyMemorySearchTool(resolveMemoryToolOptions(ctx)), { names: ["memory_search"] });
api.registerTool((ctx) => createLazyMemoryGetTool(resolveMemoryToolOptions(ctx)), { names: ["memory_get"] });

2. full profile is not filtering these tools

/usr/lib/node_modules/openclaw/dist/tool-policy-shared-MN-JZIlL.js

browser, memory_search, and memory_get are known tool IDs. memory_search and memory_get are in the coding profile; browser has no default profile, but full is intentionally unrestricted:

const CORE_TOOL_PROFILES = {
  minimal: { allow: listCoreToolIdsForProfile("minimal") },
  coding: { allow: [...listCoreToolIdsForProfile("coding"), "bundle-mcp"] },
  messaging: { allow: [...listCoreToolIdsForProfile("messaging"), "bundle-mcp"] },
  full: {}
};

function resolveCoreToolProfilePolicy(profile) {
  if (!profile) return;
  const resolved = CORE_TOOL_PROFILES[profile];
  if (!resolved) return;
  if (!resolved.allow && !resolved.deny) return;
  return { ... };
}

For profile: "full", resolveToolProfilePolicy("full") returns undefined, so there is no profile allowlist that would remove browser/memory tools.

3. localModelLean is also not the cause here

/usr/lib/node_modules/openclaw/dist/pi-tools-J9lznXGK.js

function applyModelProviderToolPolicy(tools, params) {
  if (params?.config?.agents?.defaults?.experimental?.localModelLean === true) {
    const leanDeny = new Set([
      "browser",
      "cron",
      "message"
    ]);
    tools = tools.filter((tool) => !leanDeny.has(tool.name));
  }
  ...
}

This setting was not enabled in the inspected config. Also, even if it were enabled, it would only explain browser; it would not explain memory_search and memory_get disappearing.

4. The failure occurs in plugin tool resolution

/usr/lib/node_modules/openclaw/dist/tools-D9CRirqH.js

resolvePluginTools() calculates onlyPluginIds, then tries to reuse/load a runtime plugin registry:

const onlyPluginIds = resolvePluginToolRuntimePluginIds({ ... });
...
const registry = resolvePluginToolRegistry({
  loadOptions: buildPluginRuntimeLoadOptions(context, {
    activate: false,
    toolDiscovery: true,
    onlyPluginIds: runtimePluginIds,
    runtimeOptions
  }),
  onlyPluginIds: runtimePluginIds
});
if (!registry) return tools;

In the failing environment, the equivalent onlyPluginIds set includes enabled plugin IDs such as browser and memory-core, but also disabled bundled plugins that merely declare tool contracts:

[
  "browser",
  "file-transfer",
  "firecrawl",
  "llm-task",
  "memory-core",
  "memory-wiki",
  "skill-workshop",
  "tavily",
  "xai"
]

However, several of those bundled plugins are disabled (firecrawl, llm-task, memory-wiki, skill-workshop, tavily). They are included because the manifest/control-plane availability helper treats bundled plugins as available regardless of enabled state:

/usr/lib/node_modules/openclaw/dist/manifest-contract-eligibility-Bm6pc8ez.js

function isManifestPluginAvailableForControlPlane(params) {
  if (params.plugin.origin === "bundled") return true;
  return isInstalledPluginEnabled(params.snapshot.index, params.plugin.id, params.config);
}

The loader then produces a registry where those disabled plugins are present with status "disabled", while enabled ones are loaded:

plugins: [
  ["browser", "loaded"],
  ["file-transfer", "loaded"],
  ["firecrawl", "disabled"],
  ["llm-task", "disabled"],
  ["memory-core", "loaded"],
  ["memory-wiki", "disabled"],
  ["skill-workshop", "disabled"],
  ["tavily", "disabled"],
  ["xai", "loaded"]
]

The registry itself contains the expected tool factories for the enabled plugins:

registry.tools includes:
- browser        declared ["browser"]
- memory-core    ["memory_search"] declared ["memory_get", "memory_search"]
- memory-core    ["memory_get"]    declared ["memory_get", "memory_search"]

But resolvePluginToolRegistry() calls getLoadedRuntimePluginRegistry() with requiredPluginIds containing the disabled plugin IDs too.

/usr/lib/node_modules/openclaw/dist/active-runtime-registry-DkOuNyXa.js

function registryContainsPluginIds(registry, pluginIds) {
  const loaded = new Set();
  for (const plugin of registry.plugins ?? [])
    if (plugin.status === void 0 || plugin.status === "loaded") loaded.add(plugin.id);
  ...
  return pluginIds.every((pluginId) => loaded.has(pluginId));
}

Because disabled required plugin IDs are not in the loaded set, registryContainsPluginIds() returns false. Therefore getLoadedRuntimePluginRegistry() returns undefined, even though the registry contains valid loaded tools for browser and memory-core.

Then resolvePluginToolRegistry() returns no registry, and resolvePluginTools() returns an empty plugin tool list:

if (!registry) return tools; // tools is empty unless descriptor cache already supplied something

This drops all plugin-backed tools, including unrelated enabled plugins.

Why this looks like the concrete filtering layer

The tools are not removed by:

  • tools.profile: full resolves to no restrictive policy.
  • applyToolPolicyPipeline(): it only filters the list it receives; by this point the plugin tools were never appended.
  • applyModelProviderToolPolicy() / localModelLean: not enabled, and does not explain memory tools.
  • gateway.tools deny list: relevant to Gateway HTTP tool invocation, not the Pi agent tool surface; it also does not include browser/memory.

The effective drop happens before the final policy pipeline, in plugin tool registry resolution:

createOpenClawCodingTools()
  -> createOpenClawTools()
    -> resolveOpenClawPluginToolsForOptions()
      -> resolvePluginTools()
        -> resolvePluginToolRuntimePluginIds()
           includes disabled bundled plugins with tool contracts
        -> resolvePluginToolRegistry()
          -> getLoadedRuntimePluginRegistry(requiredPluginIds = includes disabled plugins)
            -> registryContainsPluginIds() rejects registry
        -> returns [] plugin tools

Suggested fix direction

One of these would likely fix it:

  1. Make resolvePluginToolRuntimePluginIds() exclude disabled plugins from onlyPluginIds when resolving runtime tool factories, even for bundled plugins.
  2. Or make registryContainsPluginIds() treat requiredPluginIds as satisfied if the plugin is present but disabled due to config, instead of rejecting the whole registry.
  3. Or pass only the subset of actually loadable/enabled plugin IDs as requiredPluginIds to getLoadedRuntimePluginRegistry().

Option 1 seems the least surprising: disabled plugin IDs should not be required for tool discovery, and should not block enabled plugin tools from loading.

Notes

No private paths, usernames, tokens, IPs, or local workspace identifiers are included here. The above details are limited to OpenClaw version, sanitized config shape, public bundled plugin IDs, and source/dist function names relevant to the bug.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions