From 4ca9cd7e5e28506c4e47c5aeb98e8acb036b048e Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Sun, 15 Mar 2026 17:20:40 +0000 Subject: [PATCH] Plugins: extract registry registration actions --- .../plugin-registry-registrations.test.ts | 69 ++++ .../plugin-registry-registrations.ts | 338 +++++++++++++++++ src/extension-host/plugin-registry.ts | 358 ++---------------- 3 files changed, 437 insertions(+), 328 deletions(-) create mode 100644 src/extension-host/plugin-registry-registrations.test.ts create mode 100644 src/extension-host/plugin-registry-registrations.ts diff --git a/src/extension-host/plugin-registry-registrations.test.ts b/src/extension-host/plugin-registry-registrations.test.ts new file mode 100644 index 00000000000..6604553d69e --- /dev/null +++ b/src/extension-host/plugin-registry-registrations.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from "vitest"; +import { createEmptyPluginRegistry, type PluginRecord } from "../plugins/registry.js"; +import { createExtensionHostPluginRegistrationActions } from "./plugin-registry-registrations.js"; + +function createRecord(): PluginRecord { + return { + id: "demo", + name: "Demo", + source: "/plugins/demo.ts", + origin: "workspace", + enabled: true, + status: "loaded", + toolNames: [], + hookNames: [], + channelIds: [], + providerIds: [], + gatewayMethods: [], + cliCommands: [], + services: [], + commands: [], + httpRoutes: 0, + hookCount: 0, + configSchema: false, + }; +} + +describe("extension host plugin registry registrations", () => { + it("reports gateway-method collisions against core methods", () => { + const registry = createEmptyPluginRegistry(); + const actions = createExtensionHostPluginRegistrationActions({ + registry, + coreGatewayMethods: new Set(["ping"]), + pushDiagnostic: (diag) => { + registry.diagnostics.push(diag); + }, + }); + + actions.registerGatewayMethod(createRecord(), "ping", (() => {}) as never); + + expect(registry.gatewayHandlers.ping).toBeUndefined(); + expect(registry.diagnostics).toContainEqual( + expect.objectContaining({ + level: "error", + pluginId: "demo", + }), + ); + }); + + it("reports invalid context-engine registrations through the host-owned action helper", () => { + const registry = createEmptyPluginRegistry(); + const actions = createExtensionHostPluginRegistrationActions({ + registry, + coreGatewayMethods: new Set(), + pushDiagnostic: (diag) => { + registry.diagnostics.push(diag); + }, + }); + + actions.registerContextEngine(createRecord(), " ", (() => ({})) as never); + + expect(registry.diagnostics).toContainEqual( + expect.objectContaining({ + level: "error", + pluginId: "demo", + message: "context engine registration missing id", + }), + ); + }); +}); diff --git a/src/extension-host/plugin-registry-registrations.ts b/src/extension-host/plugin-registry-registrations.ts new file mode 100644 index 00000000000..2db4484c713 --- /dev/null +++ b/src/extension-host/plugin-registry-registrations.ts @@ -0,0 +1,338 @@ +import type { AnyAgentTool } from "../agents/tools/common.js"; +import type { ChannelPlugin } from "../channels/plugins/types.js"; +import { registerContextEngine as registerLegacyContextEngine } from "../context-engine/registry.js"; +import type { GatewayRequestHandler } from "../gateway/server-methods/types.js"; +import { registerInternalHook } from "../hooks/internal-hooks.js"; +import type { PluginRecord, PluginRegistry } from "../plugins/registry.js"; +import type { + PluginHookHandlerMap, + PluginHookName, + OpenClawPluginApi, + OpenClawPluginChannelRegistration, + OpenClawPluginCliRegistrar, + OpenClawPluginHookOptions, + OpenClawPluginHttpRouteParams, + OpenClawPluginService, + OpenClawPluginToolFactory, + PluginHookRegistration as TypedPluginHookRegistration, +} from "../plugins/types.js"; +import { + applyExtensionHostTypedHookPolicy, + bridgeExtensionHostLegacyHooks, +} from "./hook-compat.js"; +import { pushExtensionHostRegistryDiagnostic } from "./plugin-registry-compat.js"; +import { + addExtensionChannelRegistration, + addExtensionCliRegistration, + addExtensionContextEngineRegistration, + addExtensionGatewayMethodRegistration, + addExtensionLegacyHookRegistration, + addExtensionHttpRouteRegistration, + addExtensionServiceRegistration, + addExtensionToolRegistration, + addExtensionTypedHookRegistration, +} from "./registry-writes.js"; +import { + resolveExtensionChannelRegistration, + resolveExtensionCliRegistration, + resolveExtensionContextEngineRegistration, + resolveExtensionGatewayMethodRegistration, + resolveExtensionLegacyHookRegistration, + resolveExtensionHttpRouteRegistration, + resolveExtensionServiceRegistration, + resolveExtensionToolRegistration, + resolveExtensionTypedHookRegistration, +} from "./runtime-registrations.js"; + +export type PluginTypedHookPolicy = { + allowPromptInjection?: boolean; +}; + +export function createExtensionHostPluginRegistrationActions(params: { + registry: PluginRegistry; + coreGatewayMethods: Set; +}) { + const { registry, coreGatewayMethods } = params; + + const registerTool = ( + record: PluginRecord, + tool: AnyAgentTool | OpenClawPluginToolFactory, + opts?: { name?: string; names?: string[]; optional?: boolean }, + ) => { + const result = resolveExtensionToolRegistration({ + ownerPluginId: record.id, + ownerSource: record.source, + tool, + opts, + }); + addExtensionToolRegistration({ registry, record, names: result.names, entry: result.entry }); + }; + + const registerHook = ( + record: PluginRecord, + events: string | string[], + handler: Parameters[1], + opts: OpenClawPluginHookOptions | undefined, + config: OpenClawPluginApi["config"], + ) => { + const normalized = resolveExtensionLegacyHookRegistration({ + ownerPluginId: record.id, + ownerSource: record.source, + events, + handler, + opts, + }); + if (!normalized.ok) { + pushExtensionHostRegistryDiagnostic({ + registry, + level: "warn", + pluginId: record.id, + source: record.source, + message: normalized.message, + }); + return; + } + addExtensionLegacyHookRegistration({ + registry, + record, + hookName: normalized.hookName, + entry: normalized.entry, + events: normalized.events, + }); + + bridgeExtensionHostLegacyHooks({ + events: normalized.events, + handler, + hookSystemEnabled: config?.hooks?.internal?.enabled === true, + register: opts?.register, + registerHook: registerInternalHook, + }); + }; + + const registerGatewayMethod = ( + record: PluginRecord, + method: string, + handler: GatewayRequestHandler, + ) => { + const result = resolveExtensionGatewayMethodRegistration({ + existing: registry.gatewayHandlers, + coreGatewayMethods, + method, + handler, + }); + if (!result.ok) { + pushExtensionHostRegistryDiagnostic({ + registry, + level: "error", + pluginId: record.id, + source: record.source, + message: result.message, + }); + return; + } + addExtensionGatewayMethodRegistration({ + registry, + record, + method: result.method, + handler: result.handler, + }); + }; + + const registerHttpRoute = (record: PluginRecord, route: OpenClawPluginHttpRouteParams) => { + const result = resolveExtensionHttpRouteRegistration({ + existing: registry.httpRoutes, + ownerPluginId: record.id, + ownerSource: record.source, + route, + }); + if (!result.ok) { + pushExtensionHostRegistryDiagnostic({ + registry, + level: result.message === "http route registration missing path" ? "warn" : "error", + pluginId: record.id, + source: record.source, + message: result.message, + }); + return; + } + if (result.action === "replace") { + addExtensionHttpRouteRegistration({ + registry, + record, + action: "replace", + existingIndex: result.existingIndex, + entry: result.entry, + }); + return; + } + addExtensionHttpRouteRegistration({ + registry, + record, + action: "append", + entry: result.entry, + }); + }; + + const registerChannel = ( + record: PluginRecord, + registration: OpenClawPluginChannelRegistration | ChannelPlugin, + ) => { + const result = resolveExtensionChannelRegistration({ + existing: registry.channels, + ownerPluginId: record.id, + ownerSource: record.source, + registration, + }); + if (!result.ok) { + pushExtensionHostRegistryDiagnostic({ + registry, + level: "error", + pluginId: record.id, + source: record.source, + message: result.message, + }); + return; + } + addExtensionChannelRegistration({ + registry, + record, + channelId: result.channelId, + entry: result.entry, + }); + }; + + const registerCli = ( + record: PluginRecord, + registrar: OpenClawPluginCliRegistrar, + opts?: { commands?: string[] }, + ) => { + const result = resolveExtensionCliRegistration({ + ownerPluginId: record.id, + ownerSource: record.source, + registrar, + opts, + }); + addExtensionCliRegistration({ + registry, + record, + commands: result.commands, + entry: result.entry, + }); + }; + + const registerService = (record: PluginRecord, service: OpenClawPluginService) => { + const result = resolveExtensionServiceRegistration({ + ownerPluginId: record.id, + ownerSource: record.source, + service, + }); + if (!result.ok) { + return; + } + addExtensionServiceRegistration({ + registry, + record, + serviceId: result.serviceId, + entry: result.entry, + }); + }; + + const registerTypedHook = ( + record: PluginRecord, + hookName: K, + handler: PluginHookHandlerMap[K], + opts?: { priority?: number }, + policy?: PluginTypedHookPolicy, + ) => { + const normalized = resolveExtensionTypedHookRegistration({ + ownerPluginId: record.id, + ownerSource: record.source, + hookName, + handler, + priority: opts?.priority, + }); + if (!normalized.ok) { + pushExtensionHostRegistryDiagnostic({ + registry, + level: "warn", + pluginId: record.id, + source: record.source, + message: normalized.message, + }); + return; + } + const policyResult = applyExtensionHostTypedHookPolicy({ + hookName: normalized.hookName, + handler, + policy, + blockedMessage: `typed hook "${normalized.hookName}" blocked by plugins.entries.${record.id}.hooks.allowPromptInjection=false`, + constrainedMessage: `typed hook "${normalized.hookName}" prompt fields constrained by plugins.entries.${record.id}.hooks.allowPromptInjection=false`, + }); + if (!policyResult.ok) { + pushExtensionHostRegistryDiagnostic({ + registry, + level: "warn", + pluginId: record.id, + source: record.source, + message: policyResult.message, + }); + return; + } + if (policyResult.warningMessage) { + pushExtensionHostRegistryDiagnostic({ + registry, + level: "warn", + pluginId: record.id, + source: record.source, + message: policyResult.warningMessage, + }); + } + addExtensionTypedHookRegistration({ + registry, + record, + entry: { + ...normalized.entry, + pluginId: record.id, + hookName: normalized.hookName, + handler: policyResult.entryHandler, + } as TypedPluginHookRegistration, + }); + }; + + const registerContextEngine = ( + record: PluginRecord, + engineId: string, + factory: Parameters[1], + ) => { + const result = resolveExtensionContextEngineRegistration({ + engineId, + factory, + }); + if (!result.ok) { + pushExtensionHostRegistryDiagnostic({ + registry, + level: "error", + pluginId: record.id, + source: record.source, + message: result.message, + }); + return; + } + addExtensionContextEngineRegistration({ + entry: result.entry, + registerEngine: registerLegacyContextEngine, + }); + }; + + return { + registerTool, + registerHook, + registerGatewayMethod, + registerHttpRoute, + registerChannel, + registerCli, + registerService, + registerTypedHook, + registerContextEngine, + }; +} diff --git a/src/extension-host/plugin-registry.ts b/src/extension-host/plugin-registry.ts index 63a4764a12a..b4d1b55e8f3 100644 --- a/src/extension-host/plugin-registry.ts +++ b/src/extension-host/plugin-registry.ts @@ -1,62 +1,23 @@ -import type { AnyAgentTool } from "../agents/tools/common.js"; -import type { ChannelPlugin } from "../channels/plugins/types.js"; -import { registerContextEngine } from "../context-engine/registry.js"; -import { - applyExtensionHostTypedHookPolicy, - bridgeExtensionHostLegacyHooks, -} from "../extension-host/hook-compat.js"; -import { createExtensionHostPluginApi } from "../extension-host/plugin-api.js"; -import { - addExtensionChannelRegistration, - addExtensionCliRegistration, - addExtensionCommandRegistration, - addExtensionContextEngineRegistration, - addExtensionGatewayMethodRegistration, - addExtensionLegacyHookRegistration, - addExtensionHttpRouteRegistration, - addExtensionProviderRegistration, - addExtensionServiceRegistration, - addExtensionToolRegistration, - addExtensionTypedHookRegistration, -} from "../extension-host/registry-writes.js"; -import { - resolveExtensionChannelRegistration, - resolveExtensionCliRegistration, - resolveExtensionContextEngineRegistration, - resolveExtensionGatewayMethodRegistration, - resolveExtensionLegacyHookRegistration, - resolveExtensionHttpRouteRegistration, - resolveExtensionServiceRegistration, - resolveExtensionToolRegistration, - resolveExtensionTypedHookRegistration, -} from "../extension-host/runtime-registrations.js"; -import type { GatewayRequestHandler } from "../gateway/server-methods/types.js"; -import { registerInternalHook } from "../hooks/internal-hooks.js"; import type { PluginRecord, PluginRegistry, PluginRegistryParams } from "../plugins/registry.js"; import type { PluginDiagnostic, - PluginHookHandlerMap, - PluginHookName, OpenClawPluginApi, - OpenClawPluginChannelRegistration, - OpenClawPluginCliRegistrar, OpenClawPluginCommandDefinition, - OpenClawPluginHookOptions, - OpenClawPluginHttpRouteParams, - OpenClawPluginService, - OpenClawPluginToolFactory, ProviderPlugin, - PluginHookRegistration as TypedPluginHookRegistration, } from "../plugins/types.js"; +import { createExtensionHostPluginApi } from "./plugin-api.js"; import { - pushExtensionHostRegistryDiagnostic, resolveExtensionHostCommandCompatibility, resolveExtensionHostProviderCompatibility, } from "./plugin-registry-compat.js"; - -type PluginTypedHookPolicy = { - allowPromptInjection?: boolean; -}; +import { + createExtensionHostPluginRegistrationActions, + type PluginTypedHookPolicy, +} from "./plugin-registry-registrations.js"; +import { + addExtensionCommandRegistration, + addExtensionProviderRegistration, +} from "./registry-writes.js"; export function createExtensionHostPluginRegistry(params: { registry: PluginRegistry; @@ -67,153 +28,10 @@ export function createExtensionHostPluginRegistry(params: { const pushDiagnostic = (diag: PluginDiagnostic) => { registry.diagnostics.push(diag); }; - - const registerTool = ( - record: PluginRecord, - tool: AnyAgentTool | OpenClawPluginToolFactory, - opts?: { name?: string; names?: string[]; optional?: boolean }, - ) => { - const result = resolveExtensionToolRegistration({ - ownerPluginId: record.id, - ownerSource: record.source, - tool, - opts, - }); - addExtensionToolRegistration({ registry, record, names: result.names, entry: result.entry }); - }; - - const registerHook = ( - record: PluginRecord, - events: string | string[], - handler: Parameters[1], - opts: OpenClawPluginHookOptions | undefined, - config: OpenClawPluginApi["config"], - ) => { - const normalized = resolveExtensionLegacyHookRegistration({ - ownerPluginId: record.id, - ownerSource: record.source, - events, - handler, - opts, - }); - if (!normalized.ok) { - pushExtensionHostRegistryDiagnostic({ - registry, - level: "warn", - pluginId: record.id, - source: record.source, - message: normalized.message, - }); - return; - } - addExtensionLegacyHookRegistration({ - registry, - record, - hookName: normalized.hookName, - entry: normalized.entry, - events: normalized.events, - }); - - bridgeExtensionHostLegacyHooks({ - events: normalized.events, - handler, - hookSystemEnabled: config?.hooks?.internal?.enabled === true, - register: opts?.register, - registerHook: registerInternalHook, - }); - }; - - const registerGatewayMethod = ( - record: PluginRecord, - method: string, - handler: GatewayRequestHandler, - ) => { - const result = resolveExtensionGatewayMethodRegistration({ - existing: registry.gatewayHandlers, - coreGatewayMethods, - method, - handler, - }); - if (!result.ok) { - pushExtensionHostRegistryDiagnostic({ - registry, - level: "error", - pluginId: record.id, - source: record.source, - message: result.message, - }); - return; - } - addExtensionGatewayMethodRegistration({ - registry, - record, - method: result.method, - handler: result.handler, - }); - }; - - const registerHttpRoute = (record: PluginRecord, route: OpenClawPluginHttpRouteParams) => { - const result = resolveExtensionHttpRouteRegistration({ - existing: registry.httpRoutes, - ownerPluginId: record.id, - ownerSource: record.source, - route, - }); - if (!result.ok) { - pushExtensionHostRegistryDiagnostic({ - registry, - level: result.message === "http route registration missing path" ? "warn" : "error", - pluginId: record.id, - source: record.source, - message: result.message, - }); - return; - } - if (result.action === "replace") { - addExtensionHttpRouteRegistration({ - registry, - record, - action: "replace", - existingIndex: result.existingIndex, - entry: result.entry, - }); - return; - } - addExtensionHttpRouteRegistration({ - registry, - record, - action: "append", - entry: result.entry, - }); - }; - - const registerChannel = ( - record: PluginRecord, - registration: OpenClawPluginChannelRegistration | ChannelPlugin, - ) => { - const result = resolveExtensionChannelRegistration({ - existing: registry.channels, - ownerPluginId: record.id, - ownerSource: record.source, - registration, - }); - if (!result.ok) { - pushExtensionHostRegistryDiagnostic({ - registry, - level: "error", - pluginId: record.id, - source: record.source, - message: result.message, - }); - return; - } - addExtensionChannelRegistration({ - registry, - record, - channelId: result.channelId, - entry: result.entry, - }); - }; + const actions = createExtensionHostPluginRegistrationActions({ + registry, + coreGatewayMethods, + }); const registerProvider = (record: PluginRecord, provider: ProviderPlugin) => { const result = resolveExtensionHostProviderCompatibility({ @@ -232,42 +50,6 @@ export function createExtensionHostPluginRegistry(params: { }); }; - const registerCli = ( - record: PluginRecord, - registrar: OpenClawPluginCliRegistrar, - opts?: { commands?: string[] }, - ) => { - const result = resolveExtensionCliRegistration({ - ownerPluginId: record.id, - ownerSource: record.source, - registrar, - opts, - }); - addExtensionCliRegistration({ - registry, - record, - commands: result.commands, - entry: result.entry, - }); - }; - - const registerService = (record: PluginRecord, service: OpenClawPluginService) => { - const result = resolveExtensionServiceRegistration({ - ownerPluginId: record.id, - ownerSource: record.source, - service, - }); - if (!result.ok) { - return; - } - addExtensionServiceRegistration({ - registry, - record, - serviceId: result.serviceId, - entry: result.entry, - }); - }; - const registerCommand = (record: PluginRecord, command: OpenClawPluginCommandDefinition) => { const normalized = resolveExtensionHostCommandCompatibility({ registry, record, command }); if (!normalized.ok) { @@ -281,68 +63,6 @@ export function createExtensionHostPluginRegistry(params: { }); }; - const registerTypedHook = ( - record: PluginRecord, - hookName: K, - handler: PluginHookHandlerMap[K], - opts?: { priority?: number }, - policy?: PluginTypedHookPolicy, - ) => { - const normalized = resolveExtensionTypedHookRegistration({ - ownerPluginId: record.id, - ownerSource: record.source, - hookName, - handler, - priority: opts?.priority, - }); - if (!normalized.ok) { - pushExtensionHostRegistryDiagnostic({ - registry, - level: "warn", - pluginId: record.id, - source: record.source, - message: normalized.message, - }); - return; - } - const policyResult = applyExtensionHostTypedHookPolicy({ - hookName: normalized.hookName, - handler, - policy, - blockedMessage: `typed hook "${normalized.hookName}" blocked by plugins.entries.${record.id}.hooks.allowPromptInjection=false`, - constrainedMessage: `typed hook "${normalized.hookName}" prompt fields constrained by plugins.entries.${record.id}.hooks.allowPromptInjection=false`, - }); - if (!policyResult.ok) { - pushExtensionHostRegistryDiagnostic({ - registry, - level: "warn", - pluginId: record.id, - source: record.source, - message: policyResult.message, - }); - return; - } - if (policyResult.warningMessage) { - pushExtensionHostRegistryDiagnostic({ - registry, - level: "warn", - pluginId: record.id, - source: record.source, - message: policyResult.warningMessage, - }); - } - addExtensionTypedHookRegistration({ - registry, - record, - entry: { - ...normalized.entry, - pluginId: record.id, - hookName: normalized.hookName, - handler: policyResult.entryHandler, - } as TypedPluginHookRegistration, - }); - }; - const createApi = ( record: PluginRecord, params: { @@ -357,38 +77,20 @@ export function createExtensionHostPluginRegistry(params: { logger: registryParams.logger, config: params.config, pluginConfig: params.pluginConfig, - registerTool: (tool, opts) => registerTool(record, tool, opts), + registerTool: (tool, opts) => actions.registerTool(record, tool, opts), registerHook: (events, handler, opts) => - registerHook(record, events, handler, opts, params.config), - registerHttpRoute: (routeParams) => registerHttpRoute(record, routeParams), - registerChannel: (registration) => registerChannel(record, registration as never), + actions.registerHook(record, events, handler, opts, params.config), + registerHttpRoute: (routeParams) => actions.registerHttpRoute(record, routeParams), + registerChannel: (registration) => actions.registerChannel(record, registration as never), registerProvider: (provider) => registerProvider(record, provider), - registerGatewayMethod: (method, handler) => registerGatewayMethod(record, method, handler), - registerCli: (registrar, opts) => registerCli(record, registrar, opts), - registerService: (service) => registerService(record, service), + registerGatewayMethod: (method, handler) => + actions.registerGatewayMethod(record, method, handler), + registerCli: (registrar, opts) => actions.registerCli(record, registrar, opts), + registerService: (service) => actions.registerService(record, service), registerCommand: (command) => registerCommand(record, command), - registerContextEngine: (id, factory) => { - const result = resolveExtensionContextEngineRegistration({ - engineId: id, - factory, - }); - if (!result.ok) { - pushExtensionHostRegistryDiagnostic({ - registry, - level: "error", - pluginId: record.id, - source: record.source, - message: result.message, - }); - return; - } - addExtensionContextEngineRegistration({ - entry: result.entry, - registerEngine: registerContextEngine, - }); - }, + registerContextEngine: (id, factory) => actions.registerContextEngine(record, id, factory), on: (hookName, handler, opts) => - registerTypedHook(record, hookName, handler, opts, params.hookPolicy), + actions.registerTypedHook(record, hookName, handler, opts, params.hookPolicy), }); }; @@ -396,14 +98,14 @@ export function createExtensionHostPluginRegistry(params: { registry, createApi, pushDiagnostic, - registerTool, - registerChannel, + registerTool: actions.registerTool, + registerChannel: actions.registerChannel, registerProvider, - registerGatewayMethod, - registerCli, - registerService, + registerGatewayMethod: actions.registerGatewayMethod, + registerCli: actions.registerCli, + registerService: actions.registerService, registerCommand, - registerHook, - registerTypedHook, + registerHook: actions.registerHook, + registerTypedHook: actions.registerTypedHook, }; }