Plugin Tools

Deep Dive

Tool resolution, allowlists, name normalization, and conflict management

The plugin tools module manages the full lifecycle of plugin-provided tools — from registration through name normalization to conflict handling. It uses WeakMap-based metadata tracking to annotate tools without modifying their shape, and supports three levels of allowlist matching for fine-grained control.

Name Normalization Allowlist Matching WeakMap Metadata Conflict Detection

📚 What is this & why does it matter?

Plugins can register tools with arbitrary names, but the agent needs a clean, deduplicated set of tools with no naming collisions. This module is the quality control checkpoint — it normalizes names for consistent comparison, checks each tool against an allowlist, detects conflicts with built-in tools, and tracks metadata about each tool’s origin without modifying the tool object itself.

💡 Real-world analogy

Think of a patent office (the plugin tools module) processing invention applications (tool registrations). Each application goes through three desks: the naming desk (normalizeToolName — ensures consistent formatting), the approval desk (allowlist check — verifies the invention is on the approved list), and the conflict desk (name collision detection — rejects if an identical patent already exists). Each approved patent gets a sticky note (WeakMap metadata) on the back with the inventor’s ID, but the patent document itself is never altered.

Contents

Overview

Primary Role

Manage the lifecycle of plugin-provided tools — from registration through resolution to conflict handling.

Main Inputs

Tool definitions from plugins, allowlist configuration, existing built-in tool names.

Key Decisions

How to normalize tool names, whether optional tools pass the allowlist, how to handle duplicate names.

Outputs

Clean, deduplicated AnyAgentTool[] with metadata tracking via WeakMap.

flowchart TD
  subgraph Input["Tool Sources"]
    PluginTools["Plugin registerTool()"]
    MCPTools["MCP Server Tools"]
    BuiltIn["Built-in Tools"]
  end
  subgraph Resolution["Resolution Pipeline"]
    Normalize["normalizeToolName()"]
    AllowCheck["Allowlist Check"]
    ConflictCheck["Conflict Detection"]
    MetaAttach["Attach PluginToolMeta"]
  end
  subgraph Output["Result"]
    Final["AnyAgentTool[]"]
    Meta["WeakMap metadata"]
  end
  PluginTools --> Normalize
  MCPTools --> Normalize
  Normalize --> AllowCheck
  AllowCheck --> ConflictCheck
  BuiltIn --> ConflictCheck
  ConflictCheck --> MetaAttach
  MetaAttach --> Final
  MetaAttach --> Meta

Tools flow from three sources through a resolution pipeline. Plugin-registered and MCP server tools are normalized and filtered, while built-in tools serve as the conflict reference set. The output is a clean array of tools with WeakMap-attached metadata tracking their origin.

How It Works

Plugin tool management operates through four key mechanisms: name normalization, allowlist matching, metadata attachment, and conflict detection.

Mechanism 1: Name Normalization

normalizeToolName() standardizes tool names for consistent comparison. It converts to lowercase and strips hyphens and underscores, ensuring that my-tool, my_tool, and MyTool are treated as equivalent.

// tool-policy.ts — name normalization
export function normalizeToolName(name: string): string {
  return name.toLowerCase().replace(/[-_]/g, '');
}

// Examples:
// normalizeToolName("my-tool")    → "mytool"
// normalizeToolName("My_Tool")    → "mytool"
// normalizeToolName("myTool")     → "mytool"
Key detail: Normalization is intentionally aggressive — it removes all hyphens and underscores. This catches common naming convention differences between plugins (kebab-case vs snake_case vs camelCase) but means that my-tool and mytool are considered the same name.

Mechanism 2: Allowlist Matching

normalizeAllowlist() converts the user-provided string list into a Set<string> of normalized names. isOptionalToolAllowed() then checks three levels in order: exact tool name, plugin ID, or the group:plugins wildcard.

// tool-policy.ts — allowlist normalization and matching
export function normalizeAllowlist(allowlist: string[]): Set<string> {
  return new Set(allowlist.map(s => s.trim().toLowerCase()));
}

export function isOptionalToolAllowed(
  toolName: string,
  pluginId: string,
  allowlist: Set<string>,
): boolean {
  // Level 1: Exact tool name match
  if (allowlist.has(toolName)) return true;

  // Level 2: Plugin ID match (all tools from this plugin)
  if (allowlist.has(pluginId)) return true;

  // Level 3: Wildcard (all plugin tools allowed)
  if (allowlist.has('group:plugins')) return true;

  return false;
}
Empty allowlist means no optional tools: If the allowlist is empty (or not provided), no optional tools will pass the filter. Only required tools (those without the optional flag) bypass the allowlist entirely.

Mechanism 3: WeakMap Metadata

PluginToolMeta is attached to each tool via a module-scoped WeakMap. This tracks the originating pluginId and whether the tool is optional, without modifying the tool object’s shape or prototype. Helper functions setPluginToolMeta(), getPluginToolMeta(), and copyPluginToolMeta() manage the metadata lifecycle.

// plugin-tool-meta.ts — WeakMap metadata management
interface PluginToolMeta {
  pluginId: string;
  optional: boolean;
}

const metaMap = new WeakMap<AnyAgentTool, PluginToolMeta>();

export function setPluginToolMeta(
  tool: AnyAgentTool,
  meta: PluginToolMeta,
): void {
  metaMap.set(tool, meta);
}

export function getPluginToolMeta(
  tool: AnyAgentTool,
): PluginToolMeta | undefined {
  return metaMap.get(tool);
}

export function copyPluginToolMeta(
  source: AnyAgentTool,
  target: AnyAgentTool,
): void {
  const meta = metaMap.get(source);
  if (meta) metaMap.set(target, meta);
}
WeakMap advantage: Because the map holds weak references, tool metadata is automatically garbage collected when the tool object is no longer referenced. No manual cleanup is needed, even for long-running agent sessions.

Mechanism 4: Conflict Detection

After normalization and allowlist filtering, each tool’s normalized name is checked against the existing set of built-in tool names. The suppressNameConflicts flag controls whether conflicts are silently skipped or logged as warnings.

// tools.ts — conflict detection
const normalized = normalizeToolName(tool.name);

if (existingNames.has(normalized)) {
  if (suppressNameConflicts) {
    // Silently skip — plugin tool loses to built-in
    return false;
  }
  console.warn(
    `Plugin tool "${tool.name}" (from ${meta?.pluginId}) ` +
    `conflicts with existing tool "${normalized}"`,
  );
  return false;
}

// No conflict — add to the set for future checks
existingNames.add(normalized);

Tool Lifecycle State Machine

stateDiagram-v2
  [*] --> Registered : Plugin registers tool
  Registered --> Normalized : normalizeToolName()
  Normalized --> AllowlistCheck : Check allowlist
  AllowlistCheck --> Allowed : In allowlist
  AllowlistCheck --> Blocked : Not in allowlist
  Allowed --> ConflictCheck : Check existing names
  ConflictCheck --> Added : No conflict
  ConflictCheck --> Suppressed : Conflict + suppress
  ConflictCheck --> Warning : Conflict + no suppress
  Blocked --> [*]
  Added --> [*]
  Suppressed --> [*]
  Warning --> [*]

The state machine above shows every possible path a plugin tool can take through the resolution pipeline. A tool either ends up in the final AnyAgentTool[] (Added), is silently dropped (Blocked or Suppressed), or triggers a warning before being dropped (Warning).

Annotated Code Walkthrough

normalizeAllowlist() — convert the user’s string list to a Set of normalized names. This ensures case-insensitive and whitespace-trimmed matching throughout the pipeline.

const normalizedAllowlist = normalizeAllowlist(config.toolAllowlist ?? []);
// Input:  ["My-Tool", " GROUP:PLUGINS ", "discord-plugin"]
// Output: Set{"my-tool", "group:plugins", "discord-plugin"}

isOptionalToolAllowed() — three-level match cascade. First checks for the exact normalized tool name, then the plugin ID, then the group:plugins wildcard. First match wins, so more specific rules take priority.

// Tool "send-message" from plugin "discord-plugin"
isOptionalToolAllowed("sendmessage", "discord-plugin", allowlist);
// Check 1: allowlist.has("sendmessage")      → false
// Check 2: allowlist.has("discord-plugin")    → true → ALLOWED

PluginToolMeta WeakMap — attach metadata to the tool object without modifying its shape. The WeakMap key is the tool object reference itself, so metadata is automatically cleaned up when the tool is garbage collected.

setPluginToolMeta(tool, { pluginId: "discord-plugin", optional: true });

// Later, during logging or diagnostics:
const meta = getPluginToolMeta(tool);
console.log(`Tool from plugin: ${meta?.pluginId}`); // "discord-plugin"

Conflict detection loop — compare each tool’s normalized name against the existing set. Built-in tools are added to the set first, so they always win conflicts. Plugin tools are checked in registration order.

for (const tool of pluginTools) {
  const norm = normalizeToolName(tool.name);
  if (existingNames.has(norm)) {
    // Built-in or earlier plugin already claimed this name
    continue;
  }
  existingNames.add(norm);
  result.push(tool);
}

Edge Cases & Error Handling

ScenarioBehaviorCode Location
Empty allowlistNo optional tools are allowed; only required tools pass throughisOptionalToolAllowed()
group:plugins in allowlistAll optional plugin tools are allowed regardless of name or plugin IDisOptionalToolAllowed()
Tool name normalization edge casesmy-tool, my_tool, and MyTool all normalize to mytool and are treated as conflictsnormalizeToolName()
WeakMap garbage collectionMetadata is automatically cleaned up when tool objects are no longer referenced; no manual cleanup neededplugin-tool-meta.ts
Factory returns empty arrayNo tools from that factory are added; iteration continues to next factory without errorresolvePluginTools()

Directory Map

Source files for plugin tools
src/plugins/
├── tool-policy.ts               ← normalizeToolName(), normalizeAllowlist(), isOptionalToolAllowed()
├── plugin-tool-meta.ts          ← PluginToolMeta WeakMap, set/get/copy helpers
├── tools.ts                     ← resolvePluginTools(), conflict detection loop
├── registry.ts                  ← PluginRegistry, tool factory registration interface
└── types.ts                     ← AnyAgentTool, OpenClawPluginToolFactory type definitions