Plugins: extract registry registration actions

This commit is contained in:
Gustavo Madeira Santana 2026-03-15 17:20:40 +00:00
parent 41acacbd02
commit 4ca9cd7e5e
No known key found for this signature in database
3 changed files with 437 additions and 328 deletions

View File

@ -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",
}),
);
});
});

View File

@ -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<string>;
}) {
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<typeof registerInternalHook>[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 = <K extends PluginHookName>(
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<typeof registerLegacyContextEngine>[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,
};
}

View File

@ -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<typeof registerInternalHook>[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 = <K extends PluginHookName>(
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,
};
}