openclaw/src/plugins/registry.ts

516 lines
14 KiB
TypeScript
Raw Normal View History

import path from "node:path";
2026-01-11 12:11:12 +00:00
import type { AnyAgentTool } from "../agents/tools/common.js";
2026-01-15 02:42:41 +00:00
import type { ChannelDock } from "../channels/dock.js";
import type { ChannelPlugin } from "../channels/plugins/types.js";
2026-01-11 12:11:12 +00:00
import type {
GatewayRequestHandler,
GatewayRequestHandlers,
} from "../gateway/server-methods/types.js";
import type { HookEntry } from "../hooks/types.js";
import type { PluginRuntime } from "./runtime/types.js";
2026-01-11 12:11:12 +00:00
import type {
2026-01-30 03:15:10 +01:00
OpenClawPluginApi,
OpenClawPluginChannelRegistration,
OpenClawPluginCliRegistrar,
OpenClawPluginCommandDefinition,
OpenClawPluginHttpHandler,
OpenClawPluginHttpRouteHandler,
OpenClawPluginHookOptions,
2026-01-16 00:39:22 +00:00
ProviderPlugin,
2026-01-30 03:15:10 +01:00
OpenClawPluginService,
OpenClawPluginToolContext,
OpenClawPluginToolFactory,
2026-01-12 01:16:39 +00:00
PluginConfigUiHint,
2026-01-11 12:11:12 +00:00
PluginDiagnostic,
PluginLogger,
PluginOrigin,
2026-01-18 02:12:01 +00:00
PluginKind,
PluginHookName,
PluginHookHandlerMap,
PluginHookRegistration as TypedPluginHookRegistration,
2026-01-11 12:11:12 +00:00
} from "./types.js";
import { registerInternalHook } from "../hooks/internal-hooks.js";
import { resolveUserPath } from "../utils.js";
import { registerPluginCommand } from "./commands.js";
import { normalizePluginHttpPath } from "./http-path.js";
2026-01-11 12:11:12 +00:00
export type PluginToolRegistration = {
pluginId: string;
2026-01-30 03:15:10 +01:00
factory: OpenClawPluginToolFactory;
2026-01-11 12:11:12 +00:00
names: string[];
2026-01-18 04:07:19 +00:00
optional: boolean;
2026-01-11 12:11:12 +00:00
source: string;
};
export type PluginCliRegistration = {
pluginId: string;
2026-01-30 03:15:10 +01:00
register: OpenClawPluginCliRegistrar;
2026-01-11 12:11:12 +00:00
commands: string[];
source: string;
};
export type PluginHttpRegistration = {
pluginId: string;
2026-01-30 03:15:10 +01:00
handler: OpenClawPluginHttpHandler;
source: string;
};
export type PluginHttpRouteRegistration = {
pluginId?: string;
path: string;
2026-01-30 03:15:10 +01:00
handler: OpenClawPluginHttpRouteHandler;
source?: string;
};
2026-01-15 02:42:41 +00:00
export type PluginChannelRegistration = {
pluginId: string;
plugin: ChannelPlugin;
dock?: ChannelDock;
source: string;
};
2026-01-16 00:39:22 +00:00
export type PluginProviderRegistration = {
pluginId: string;
provider: ProviderPlugin;
source: string;
};
2026-01-18 05:56:59 +00:00
export type PluginHookRegistration = {
pluginId: string;
entry: HookEntry;
events: string[];
source: string;
};
2026-01-11 12:11:12 +00:00
export type PluginServiceRegistration = {
pluginId: string;
2026-01-30 03:15:10 +01:00
service: OpenClawPluginService;
2026-01-11 12:11:12 +00:00
source: string;
};
export type PluginCommandRegistration = {
pluginId: string;
2026-01-30 03:15:10 +01:00
command: OpenClawPluginCommandDefinition;
source: string;
};
2026-01-11 12:11:12 +00:00
export type PluginRecord = {
id: string;
name: string;
version?: string;
description?: string;
2026-01-18 02:12:01 +00:00
kind?: PluginKind;
2026-01-11 12:11:12 +00:00
source: string;
origin: PluginOrigin;
workspaceDir?: string;
enabled: boolean;
status: "loaded" | "disabled" | "error";
error?: string;
toolNames: string[];
2026-01-18 05:56:59 +00:00
hookNames: string[];
2026-01-15 02:42:41 +00:00
channelIds: string[];
2026-01-16 00:39:22 +00:00
providerIds: string[];
2026-01-11 12:11:12 +00:00
gatewayMethods: string[];
cliCommands: string[];
services: string[];
commands: string[];
httpHandlers: number;
hookCount: number;
2026-01-11 12:11:12 +00:00
configSchema: boolean;
2026-01-12 01:16:39 +00:00
configUiHints?: Record<string, PluginConfigUiHint>;
configJsonSchema?: Record<string, unknown>;
2026-01-11 12:11:12 +00:00
};
export type PluginRegistry = {
plugins: PluginRecord[];
tools: PluginToolRegistration[];
2026-01-18 05:56:59 +00:00
hooks: PluginHookRegistration[];
typedHooks: TypedPluginHookRegistration[];
2026-01-15 02:42:41 +00:00
channels: PluginChannelRegistration[];
2026-01-16 00:39:22 +00:00
providers: PluginProviderRegistration[];
2026-01-11 12:11:12 +00:00
gatewayHandlers: GatewayRequestHandlers;
httpHandlers: PluginHttpRegistration[];
httpRoutes: PluginHttpRouteRegistration[];
2026-01-11 12:11:12 +00:00
cliRegistrars: PluginCliRegistration[];
services: PluginServiceRegistration[];
commands: PluginCommandRegistration[];
2026-01-11 12:11:12 +00:00
diagnostics: PluginDiagnostic[];
};
export type PluginRegistryParams = {
logger: PluginLogger;
coreGatewayHandlers?: GatewayRequestHandlers;
runtime: PluginRuntime;
2026-01-11 12:11:12 +00:00
};
export function createPluginRegistry(registryParams: PluginRegistryParams) {
const registry: PluginRegistry = {
plugins: [],
tools: [],
2026-01-18 05:56:59 +00:00
hooks: [],
typedHooks: [],
2026-01-15 02:42:41 +00:00
channels: [],
2026-01-16 00:39:22 +00:00
providers: [],
2026-01-11 12:11:12 +00:00
gatewayHandlers: {},
httpHandlers: [],
httpRoutes: [],
2026-01-11 12:11:12 +00:00
cliRegistrars: [],
services: [],
commands: [],
2026-01-11 12:11:12 +00:00
diagnostics: [],
};
const coreGatewayMethods = new Set(Object.keys(registryParams.coreGatewayHandlers ?? {}));
2026-01-11 12:11:12 +00:00
const pushDiagnostic = (diag: PluginDiagnostic) => {
registry.diagnostics.push(diag);
};
const registerTool = (
record: PluginRecord,
2026-01-30 03:15:10 +01:00
tool: AnyAgentTool | OpenClawPluginToolFactory,
2026-01-18 04:07:19 +00:00
opts?: { name?: string; names?: string[]; optional?: boolean },
2026-01-11 12:11:12 +00:00
) => {
const names = opts?.names ?? (opts?.name ? [opts.name] : []);
2026-01-18 04:07:19 +00:00
const optional = opts?.optional === true;
2026-01-30 03:15:10 +01:00
const factory: OpenClawPluginToolFactory =
typeof tool === "function" ? tool : (_ctx: OpenClawPluginToolContext) => tool;
2026-01-11 12:11:12 +00:00
if (typeof tool !== "function") {
names.push(tool.name);
}
const normalized = names.map((name) => name.trim()).filter(Boolean);
if (normalized.length > 0) {
record.toolNames.push(...normalized);
}
registry.tools.push({
pluginId: record.id,
factory,
names: normalized,
2026-01-18 04:07:19 +00:00
optional,
2026-01-11 12:11:12 +00:00
source: record.source,
});
};
2026-01-18 05:56:59 +00:00
const registerHook = (
record: PluginRecord,
events: string | string[],
handler: Parameters<typeof registerInternalHook>[1],
2026-01-30 03:15:10 +01:00
opts: OpenClawPluginHookOptions | undefined,
config: OpenClawPluginApi["config"],
2026-01-18 05:56:59 +00:00
) => {
const eventList = Array.isArray(events) ? events : [events];
const normalizedEvents = eventList.map((event) => event.trim()).filter(Boolean);
const entry = opts?.entry ?? null;
const name = entry?.hook.name ?? opts?.name?.trim();
if (!name) {
pushDiagnostic({
level: "warn",
pluginId: record.id,
source: record.source,
message: "hook registration missing name",
});
return;
}
const description = entry?.hook.description ?? opts?.description ?? "";
const hookEntry: HookEntry = entry
? {
...entry,
hook: {
...entry.hook,
name,
description,
2026-01-30 03:15:10 +01:00
source: "openclaw-plugin",
2026-01-18 05:56:59 +00:00
pluginId: record.id,
},
metadata: {
...entry.metadata,
2026-01-18 05:56:59 +00:00
events: normalizedEvents,
},
}
: {
hook: {
name,
description,
2026-01-30 03:15:10 +01:00
source: "openclaw-plugin",
2026-01-18 05:56:59 +00:00
pluginId: record.id,
filePath: record.source,
baseDir: path.dirname(record.source),
handlerPath: record.source,
},
frontmatter: {},
metadata: { events: normalizedEvents },
2026-01-18 05:56:59 +00:00
invocation: { enabled: true },
};
record.hookNames.push(name);
registry.hooks.push({
pluginId: record.id,
entry: hookEntry,
events: normalizedEvents,
source: record.source,
});
const hookSystemEnabled = config?.hooks?.internal?.enabled === true;
if (!hookSystemEnabled || opts?.register === false) {
return;
}
for (const event of normalizedEvents) {
registerInternalHook(event, handler);
}
};
2026-01-11 12:11:12 +00:00
const registerGatewayMethod = (
record: PluginRecord,
method: string,
handler: GatewayRequestHandler,
) => {
const trimmed = method.trim();
if (!trimmed) {
return;
}
2026-01-11 12:11:12 +00:00
if (coreGatewayMethods.has(trimmed) || registry.gatewayHandlers[trimmed]) {
pushDiagnostic({
level: "error",
pluginId: record.id,
source: record.source,
message: `gateway method already registered: ${trimmed}`,
});
return;
}
registry.gatewayHandlers[trimmed] = handler;
record.gatewayMethods.push(trimmed);
};
2026-01-30 03:15:10 +01:00
const registerHttpHandler = (record: PluginRecord, handler: OpenClawPluginHttpHandler) => {
record.httpHandlers += 1;
registry.httpHandlers.push({
pluginId: record.id,
handler,
source: record.source,
});
};
const registerHttpRoute = (
record: PluginRecord,
2026-01-30 03:15:10 +01:00
params: { path: string; handler: OpenClawPluginHttpRouteHandler },
) => {
const normalizedPath = normalizePluginHttpPath(params.path);
if (!normalizedPath) {
pushDiagnostic({
level: "warn",
pluginId: record.id,
source: record.source,
message: "http route registration missing path",
});
return;
}
if (registry.httpRoutes.some((entry) => entry.path === normalizedPath)) {
pushDiagnostic({
level: "error",
pluginId: record.id,
source: record.source,
message: `http route already registered: ${normalizedPath}`,
});
return;
}
record.httpHandlers += 1;
registry.httpRoutes.push({
pluginId: record.id,
path: normalizedPath,
handler: params.handler,
source: record.source,
});
};
2026-01-15 02:42:41 +00:00
const registerChannel = (
record: PluginRecord,
2026-01-30 03:15:10 +01:00
registration: OpenClawPluginChannelRegistration | ChannelPlugin,
2026-01-15 02:42:41 +00:00
) => {
const normalized =
2026-01-30 03:15:10 +01:00
typeof (registration as OpenClawPluginChannelRegistration).plugin === "object"
? (registration as OpenClawPluginChannelRegistration)
2026-01-15 02:42:41 +00:00
: { plugin: registration as ChannelPlugin };
const plugin = normalized.plugin;
const id = typeof plugin?.id === "string" ? plugin.id.trim() : String(plugin?.id ?? "").trim();
if (!id) {
pushDiagnostic({
level: "error",
pluginId: record.id,
source: record.source,
message: "channel registration missing id",
});
return;
}
record.channelIds.push(id);
registry.channels.push({
pluginId: record.id,
plugin,
dock: normalized.dock,
source: record.source,
});
};
2026-01-16 00:39:22 +00:00
const registerProvider = (record: PluginRecord, provider: ProviderPlugin) => {
const id = typeof provider?.id === "string" ? provider.id.trim() : "";
if (!id) {
pushDiagnostic({
level: "error",
pluginId: record.id,
source: record.source,
message: "provider registration missing id",
});
return;
}
const existing = registry.providers.find((entry) => entry.provider.id === id);
if (existing) {
pushDiagnostic({
level: "error",
pluginId: record.id,
source: record.source,
message: `provider already registered: ${id} (${existing.pluginId})`,
});
return;
}
record.providerIds.push(id);
registry.providers.push({
pluginId: record.id,
provider,
source: record.source,
});
};
2026-01-11 12:11:12 +00:00
const registerCli = (
record: PluginRecord,
2026-01-30 03:15:10 +01:00
registrar: OpenClawPluginCliRegistrar,
2026-01-11 12:11:12 +00:00
opts?: { commands?: string[] },
) => {
const commands = (opts?.commands ?? []).map((cmd) => cmd.trim()).filter(Boolean);
2026-01-11 12:11:12 +00:00
record.cliCommands.push(...commands);
registry.cliRegistrars.push({
pluginId: record.id,
register: registrar,
commands,
source: record.source,
});
};
2026-01-30 03:15:10 +01:00
const registerService = (record: PluginRecord, service: OpenClawPluginService) => {
2026-01-11 12:11:12 +00:00
const id = service.id.trim();
if (!id) {
return;
}
2026-01-11 12:11:12 +00:00
record.services.push(id);
registry.services.push({
pluginId: record.id,
service,
source: record.source,
});
};
2026-01-30 03:15:10 +01:00
const registerCommand = (record: PluginRecord, command: OpenClawPluginCommandDefinition) => {
const name = command.name.trim();
if (!name) {
pushDiagnostic({
level: "error",
pluginId: record.id,
source: record.source,
message: "command registration missing name",
});
return;
}
// Register with the plugin command system (validates name and checks for duplicates)
const result = registerPluginCommand(record.id, command);
if (!result.ok) {
pushDiagnostic({
level: "error",
pluginId: record.id,
source: record.source,
message: `command registration failed: ${result.error}`,
});
return;
}
record.commands.push(name);
registry.commands.push({
pluginId: record.id,
command,
source: record.source,
});
};
const registerTypedHook = <K extends PluginHookName>(
record: PluginRecord,
hookName: K,
handler: PluginHookHandlerMap[K],
opts?: { priority?: number },
) => {
record.hookCount += 1;
registry.typedHooks.push({
pluginId: record.id,
hookName,
handler,
priority: opts?.priority,
source: record.source,
} as TypedPluginHookRegistration);
};
2026-01-11 12:11:12 +00:00
const normalizeLogger = (logger: PluginLogger): PluginLogger => ({
info: logger.info,
warn: logger.warn,
error: logger.error,
debug: logger.debug,
});
const createApi = (
record: PluginRecord,
params: {
2026-01-30 03:15:10 +01:00
config: OpenClawPluginApi["config"];
2026-01-11 12:11:12 +00:00
pluginConfig?: Record<string, unknown>;
},
2026-01-30 03:15:10 +01:00
): OpenClawPluginApi => {
2026-01-11 12:11:12 +00:00
return {
id: record.id,
name: record.name,
version: record.version,
description: record.description,
source: record.source,
config: params.config,
pluginConfig: params.pluginConfig,
runtime: registryParams.runtime,
2026-01-11 12:11:12 +00:00
logger: normalizeLogger(registryParams.logger),
registerTool: (tool, opts) => registerTool(record, tool, opts),
2026-01-18 05:56:59 +00:00
registerHook: (events, handler, opts) =>
registerHook(record, events, handler, opts, params.config),
registerHttpHandler: (handler) => registerHttpHandler(record, handler),
registerHttpRoute: (params) => registerHttpRoute(record, params),
2026-01-15 02:42:41 +00:00
registerChannel: (registration) => registerChannel(record, registration),
2026-01-16 00:39:22 +00:00
registerProvider: (provider) => registerProvider(record, provider),
registerGatewayMethod: (method, handler) => registerGatewayMethod(record, method, handler),
2026-01-11 12:11:12 +00:00
registerCli: (registrar, opts) => registerCli(record, registrar, opts),
registerService: (service) => registerService(record, service),
registerCommand: (command) => registerCommand(record, command),
2026-01-11 12:11:12 +00:00
resolvePath: (input: string) => resolveUserPath(input),
on: (hookName, handler, opts) => registerTypedHook(record, hookName, handler, opts),
2026-01-11 12:11:12 +00:00
};
};
return {
registry,
createApi,
pushDiagnostic,
registerTool,
2026-01-15 02:42:41 +00:00
registerChannel,
2026-01-16 00:39:22 +00:00
registerProvider,
2026-01-11 12:11:12 +00:00
registerGatewayMethod,
registerCli,
registerService,
registerCommand,
registerHook,
registerTypedHook,
2026-01-11 12:11:12 +00:00
};
}