Primary Role
Wire plugin capabilities into the central PluginRegistry via typed registration methods.
How tools, hooks, channels, and providers are registered into the plugin registry
6 Registration Types Validation Layer Conflict Detection TypeScript
Capability Registration is the process by which plugins wire their tools, hooks, channels, providers, commands, and services into the central PluginRegistry. Each registration method validates inputs, detects conflicts, normalizes data, and indexes the capability for runtime use. This is where the plugin’s declared capabilities become real, callable entries in the system.
Think of capability registration like filing permits at a city hall. Each plugin is a business that submits applications (registration calls) for different licenses (tools, hooks, channels). City hall (the registry) checks each application for uniqueness (no two businesses with the same name), validates the paperwork (type checking), assigns a priority number (hook ordering), and files it in the correct department (capability map). Only after all permits are filed can the business operate.
Wire plugin capabilities into the central PluginRegistry via typed registration methods.
Tool definitions, hook handlers with priority, channel adapters, provider plugins.
How to validate registrations, detect conflicts (duplicate tool names), assign priorities, bind plugin identity.
Populated PluginRegistry with all capabilities indexed and ready for runtime use.
flowchart TD
subgraph Registration["Registration Methods"]
RT["registerTool()"]
RH["registerHook()"]
RC["registerChannel()"]
RP["registerProvider()"]
RCmd["registerCommand()"]
RSvc["registerService()"]
end
subgraph Validation["Validation"]
NameCheck["Tool name conflicts"]
PrioritySort["Hook priority ordering"]
ChanValidate["Channel ID validation"]
ProvNormalize["Provider normalization"]
end
subgraph Registry["Plugin Registry"]
Tools["tools Map"]
Hooks["hooks Map"]
Channels["channels Map"]
Providers["providers Map"]
Commands["commands Map"]
end
RT --> NameCheck
RH --> PrioritySort
RC --> ChanValidate
RP --> ProvNormalize
NameCheck --> Tools
PrioritySort --> Hooks
ChanValidate --> Channels
ProvNormalize --> Providers
RCmd --> Commands
RSvc --> Registry
Each registration method passes through a validation layer before the capability is indexed in the appropriate map within the PluginRegistry. Validation catches conflicts, normalizes data, and ensures type safety before capabilities become live.
Validates tool name uniqueness across all plugins, attaches PluginToolMeta (plugin id, name, version), and adds the tool definition to the tool factory list. Tool names are normalized via normalizeToolName() which lowercases and strips invalid characters.
PluginToolMeta reference so the runtime can trace any tool invocation back to the plugin that provided it.
Accepts a hook name from the PluginHookName union type, a handler function, and a priority number. Hooks are stored in priority-sorted order. At runtime, hooks with the same name execute in priority order (highest first). Equal priorities use the plugin id as a stable tiebreaker.
Validates the channel ID format, then registers all channel adapters: messaging, outbound, pairing, security, and threading. Each adapter is a typed interface that the channel must implement. The channel ID must be unique across all plugins.
Normalizes the provider via normalizeRegisteredProvider(), which extracts auth methods, builds the model catalog, and validates capabilities. The normalized provider is then registered with its auth configuration and model list.
Validates the command definition via validatePluginCommandDefinition(), checking for required fields (name, description, handler), valid command syntax, and argument schemas. Registered commands become available in the CLI and slash-command system.
Validates the route path pattern and checks for overlapping routes via findOverlappingPluginHttpRoute(). Overlapping routes (e.g., /api/foo/:id and /api/foo/:bar) are flagged as conflicts. Valid routes are added to the HTTP route table for the gateway.
sequenceDiagram
participant Plugin as Plugin
participant API as OpenClawPluginApi
participant Reg as registry.ts
participant Val as Validation
Plugin->>API: api.registerTool(toolDef)
API->>Val: Check name uniqueness
Val-->>API: OK
API->>Reg: Add to tools registry
Plugin->>API: api.registerHook('before_agent_start', handler)
API->>Reg: Add with priority to hooks map
Plugin->>API: api.registerChannel(channelPlugin)
API->>Val: Validate channel ID format
Val-->>API: OK
API->>Reg: Register channel adapters
Plugin->>API: api.registerProvider(providerPlugin)
API->>Val: normalizeRegisteredProvider()
Val-->>API: Normalized provider
API->>Reg: Register provider + auth methods
The sequence shows a typical plugin registration flow. Each capability type passes through its specific validation before being committed to the registry. Failures at any step are logged but do not block other registrations from the same plugin.
Walk through the key registration patterns in registry.ts:
Tool names are normalized to ensure consistency. The normalizeToolName() function lowercases the name, replaces spaces with underscores, and strips non-alphanumeric characters:
// Tool name normalization via normalizeToolName()
function normalizeToolName(name: string): string {
return name
.toLowerCase()
.replace(/\s+/g, "_")
.replace(/[^a-z0-9_]/g, "");
}
// Registration with name conflict check
function registerTool(toolDef: PluginToolDefinition, pluginId: string): void {
const normalized = normalizeToolName(toolDef.name);
if (toolsMap.has(normalized)) {
logger.warn("Duplicate tool name", { name: normalized, pluginId });
return;
}
toolsMap.set(normalized, {
...toolDef,
name: normalized,
meta: { pluginId, registeredAt: Date.now() },
});
}
Hooks are sorted by priority (descending — higher priority executes first). When priorities are equal, the plugin id is used as a stable tiebreaker to ensure deterministic execution order:
// Hook priority ordering — high-first, pluginId tiebreaker
function insertHook(
hookName: PluginHookName,
handler: HookHandler,
priority: number,
pluginId: string
): void {
const entry = { handler, priority, pluginId };
const list = hooksMap.get(hookName) ?? [];
list.push(entry);
list.sort((a, b) =>
b.priority !== a.priority
? b.priority - a.priority
: a.pluginId.localeCompare(b.pluginId)
);
hooksMap.set(hookName, list);
}
Providers go through normalizeRegisteredProvider() which extracts auth methods, builds the model catalog, and validates that required capabilities are declared:
// Provider normalization — auth methods, model catalog, capabilities
function normalizeRegisteredProvider(
provider: ProviderPlugin
): NormalizedProvider {
const authMethods = extractAuthMethods(provider);
const modelCatalog = buildModelCatalog(provider.models ?? []);
const capabilities = validateCapabilities(provider.capabilities);
return {
id: provider.id,
name: provider.name,
authMethods,
modelCatalog,
capabilities,
raw: provider,
};
}
Before registering an HTTP route, findOverlappingPluginHttpRoute() scans existing routes for path conflicts. Parameterized segments (e.g., :id) are treated as wildcards during comparison:
// HTTP route conflict detection via findOverlappingPluginHttpRoute()
function findOverlappingPluginHttpRoute(
newPath: string,
method: HttpMethod,
existingRoutes: PluginHttpRoute[]
): PluginHttpRoute | null {
const newSegments = newPath.split("/").filter(Boolean);
for (const route of existingRoutes) {
if (route.method !== method) continue;
const existingSegments = route.path.split("/").filter(Boolean);
if (existingSegments.length !== newSegments.length) continue;
const overlaps = existingSegments.every((seg, i) =>
seg.startsWith(":") || newSegments[i].startsWith(":") || seg === newSegments[i]
);
if (overlaps) return route;
}
return null;
}
| Scenario | Behavior | Impact |
|---|---|---|
| Duplicate tool names across plugins | Second registration rejected; warning logged with both plugin ids | First plugin’s tool wins; second plugin should rename |
| Hook priority ties | Stable sort by plugin id (lexicographic ascending) | Deterministic order, but may not match developer intent |
| Channel already registered by another plugin | Registration rejected; error logged | Only one plugin can own a channel id |
| Provider with no auth methods | Normalization succeeds but provider cannot authenticate | Provider registered but unusable until auth is configured |
| Command name conflicts | Validation rejects duplicate; warning logged | First command registration wins |
| HTTP route overlaps | findOverlappingPluginHttpRoute() detects and rejects | Existing route preserved; new route must use a different path |
OpenClawPluginApi is constructed before registration beginsregister(api) lifecycle that triggers capability registration📁 src/plugin-sdk/
├── registry.ts # Central PluginRegistry — all capability maps
├── registry-tool.ts # registerTool() + normalizeToolName()
├── registry-hook.ts # registerHook() + priority sorting
├── registry-channel.ts # registerChannel() + adapter validation
├── registry-provider.ts # registerProvider() + normalizeRegisteredProvider()
├── registry-command.ts # registerCommand() + validatePluginCommandDefinition()
├── registry-http-route.ts # registerHttpRoute() + findOverlappingPluginHttpRoute()
├── plugin-tool-meta.ts # PluginToolMeta type definition
└── plugin-hook-name.ts # PluginHookName union type