Plugins: extract plugin api facade

This commit is contained in:
Gustavo Madeira Santana 2026-03-15 17:04:07 +00:00
parent 8a2f0be664
commit a1e1dcc01a
No known key found for this signature in database
4 changed files with 201 additions and 20 deletions

View File

@ -64,6 +64,7 @@ This is an implementation checklist, not a future-design spec.
| Config validation indexing | `src/config/validation.ts`, `src/config/resolved-extension-validation.ts` | host-owned resolved registry | `moved` | Validation indexing now builds from resolved-extension records instead of flat manifest rows. |
| Config doc baseline generation | `src/config/doc-baseline.ts` | host-owned resolved registry | `moved` | Bundled plugin and channel metadata now load through the resolved-extension registry. |
| Plugin loader activation | `src/plugins/loader.ts` | extension host lifecycle + compatibility loader | `partial` | Activation now routes through `src/extension-host/activation.ts`, but discovery, enablement, provenance, module loading, and policy still live in the legacy plugin loader. |
| Plugin API compatibility facade | `src/plugins/registry.ts` | `src/extension-host/plugin-api.ts` | `partial` | Compatibility `OpenClawPluginApi` composition and logger shaping now delegate through a host-owned helper; the legacy registry still supplies the concrete registration callbacks. |
| Channel registration writes | `src/plugins/registry.ts` | host-owned channel registry | `partial` | Validation and normalization now delegate to `src/extension-host/runtime-registrations.ts`, and compatibility writes now route through `src/extension-host/registry-writes.ts`; the legacy plugin API still remains the call surface. |
| Provider registration writes | `src/plugins/registry.ts` | host-owned provider registry | `partial` | Provider normalization still happens in plugin-era validation, duplicate detection and normalized registration shape now delegate to `src/extension-host/runtime-registrations.ts`, and compatibility writes now route through `src/extension-host/registry-writes.ts`. |
| HTTP route registration writes | `src/plugins/registry.ts` | host-owned route registry | `partial` | Route validation and normalization now delegate to `src/extension-host/runtime-registrations.ts`, and compatibility append or replace writes now route through `src/extension-host/registry-writes.ts`. |

View File

@ -0,0 +1,105 @@
import { describe, expect, it, vi } from "vitest";
import type { PluginRecord } from "../plugins/registry.js";
import { createExtensionHostPluginApi, normalizeExtensionHostPluginLogger } from "./plugin-api.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 api", () => {
it("normalizes plugin logger methods", () => {
const logger = {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
};
const normalized = normalizeExtensionHostPluginLogger(logger);
normalized.info("x");
expect(logger.info).toHaveBeenCalledWith("x");
expect(normalized.debug).toBe(logger.debug);
});
it("creates a compatibility plugin api that delegates all registration calls", () => {
const callbacks = {
registerTool: vi.fn(),
registerHook: vi.fn(),
registerHttpRoute: vi.fn(),
registerChannel: vi.fn(),
registerProvider: vi.fn(),
registerGatewayMethod: vi.fn(),
registerCli: vi.fn(),
registerService: vi.fn(),
registerCommand: vi.fn(),
registerContextEngine: vi.fn(),
on: vi.fn(),
};
const api = createExtensionHostPluginApi({
record: createRecord(),
runtime: {} as never,
logger: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
},
config: {},
registerTool: callbacks.registerTool as never,
registerHook: callbacks.registerHook as never,
registerHttpRoute: callbacks.registerHttpRoute as never,
registerChannel: callbacks.registerChannel as never,
registerProvider: callbacks.registerProvider as never,
registerGatewayMethod: callbacks.registerGatewayMethod as never,
registerCli: callbacks.registerCli as never,
registerService: callbacks.registerService as never,
registerCommand: callbacks.registerCommand as never,
registerContextEngine: callbacks.registerContextEngine as never,
on: callbacks.on as never,
});
api.registerTool({ name: "tool" } as never);
api.registerHook("before_send", (() => {}) as never);
api.registerHttpRoute({ path: "/x", handler: (() => {}) as never, auth: "gateway" });
api.registerChannel({ id: "ch" } as never);
api.registerProvider({} as never);
api.registerGatewayMethod("ping", (() => {}) as never);
api.registerCli((() => {}) as never);
api.registerService({ id: "svc", start: async () => {}, stop: async () => {} } as never);
api.registerCommand({ name: "cmd", description: "demo", handler: async () => ({}) } as never);
api.registerContextEngine("engine", (() => ({}) as never) as never);
api.on("before_send" as never, (() => {}) as never);
expect(callbacks.registerTool).toHaveBeenCalledTimes(1);
expect(callbacks.registerHook).toHaveBeenCalledTimes(1);
expect(callbacks.registerHttpRoute).toHaveBeenCalledTimes(1);
expect(callbacks.registerChannel).toHaveBeenCalledTimes(1);
expect(callbacks.registerProvider).toHaveBeenCalledTimes(1);
expect(callbacks.registerGatewayMethod).toHaveBeenCalledTimes(1);
expect(callbacks.registerCli).toHaveBeenCalledTimes(1);
expect(callbacks.registerService).toHaveBeenCalledTimes(1);
expect(callbacks.registerCommand).toHaveBeenCalledTimes(1);
expect(callbacks.registerContextEngine).toHaveBeenCalledTimes(1);
expect(callbacks.on).toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,87 @@
import type { PluginRecord } from "../plugins/registry.js";
import type { PluginRuntime } from "../plugins/runtime/types.js";
import type {
OpenClawPluginApi,
OpenClawPluginChannelRegistration,
OpenClawPluginCliRegistrar,
OpenClawPluginCommandDefinition,
OpenClawPluginHttpRouteParams,
OpenClawPluginService,
OpenClawPluginToolFactory,
PluginLogger,
PluginHookName,
PluginHookHandlerMap,
ProviderPlugin,
} from "../plugins/types.js";
import { resolveUserPath } from "../utils.js";
export function normalizeExtensionHostPluginLogger(logger: PluginLogger): PluginLogger {
return {
info: logger.info,
warn: logger.warn,
error: logger.error,
debug: logger.debug,
};
}
export function createExtensionHostPluginApi(params: {
record: PluginRecord;
runtime: PluginRuntime;
logger: PluginLogger;
config: OpenClawPluginApi["config"];
pluginConfig?: Record<string, unknown>;
registerTool: (
tool: OpenClawPluginToolFactory | { name: string },
opts?: { name?: string; names?: string[]; optional?: boolean },
) => void;
registerHook: (
events: string | string[],
handler: Parameters<OpenClawPluginApi["registerHook"]>[1],
opts?: Parameters<OpenClawPluginApi["registerHook"]>[2],
) => void;
registerHttpRoute: (params: OpenClawPluginHttpRouteParams) => void;
registerChannel: (registration: OpenClawPluginChannelRegistration | object) => void;
registerProvider: (provider: ProviderPlugin) => void;
registerGatewayMethod: (
method: string,
handler: OpenClawPluginApi["registerGatewayMethod"] extends (m: string, h: infer H) => void
? H
: never,
) => void;
registerCli: (registrar: OpenClawPluginCliRegistrar, opts?: { commands?: string[] }) => void;
registerService: (service: OpenClawPluginService) => void;
registerCommand: (command: OpenClawPluginCommandDefinition) => void;
registerContextEngine: (
id: string,
factory: Parameters<OpenClawPluginApi["registerContextEngine"]>[1],
) => void;
on: <K extends PluginHookName>(
hookName: K,
handler: PluginHookHandlerMap[K],
opts?: { priority?: number },
) => void;
}): OpenClawPluginApi {
return {
id: params.record.id,
name: params.record.name,
version: params.record.version,
description: params.record.description,
source: params.record.source,
config: params.config,
pluginConfig: params.pluginConfig,
runtime: params.runtime,
logger: normalizeExtensionHostPluginLogger(params.logger),
registerTool: (tool, opts) => params.registerTool(tool as never, opts),
registerHook: (events, handler, opts) => params.registerHook(events, handler, opts),
registerHttpRoute: (routeParams) => params.registerHttpRoute(routeParams),
registerChannel: (registration) => params.registerChannel(registration),
registerProvider: (provider) => params.registerProvider(provider),
registerGatewayMethod: (method, handler) => params.registerGatewayMethod(method, handler),
registerCli: (registrar, opts) => params.registerCli(registrar, opts),
registerService: (service) => params.registerService(service),
registerCommand: (command) => params.registerCommand(command),
registerContextEngine: (id, factory) => params.registerContextEngine(id, factory),
resolvePath: (input) => resolveUserPath(input),
on: (hookName, handler, opts) => params.on(hookName as never, handler as never, opts),
};
}

View File

@ -6,6 +6,7 @@ import {
applyExtensionHostTypedHookPolicy,
bridgeExtensionHostLegacyHooks,
} from "../extension-host/hook-compat.js";
import { createExtensionHostPluginApi } from "../extension-host/plugin-api.js";
import {
addExtensionChannelRegistration,
addExtensionCliRegistration,
@ -37,7 +38,6 @@ import type {
GatewayRequestHandlers,
} from "../gateway/server-methods/types.js";
import { registerInternalHook } from "../hooks/internal-hooks.js";
import { resolveUserPath } from "../utils.js";
import { registerPluginCommand } from "./commands.js";
import { normalizeRegisteredProvider } from "./provider-validation.js";
import type { PluginRuntime } from "./runtime/types.js";
@ -514,13 +514,6 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
});
};
const normalizeLogger = (logger: PluginLogger): PluginLogger => ({
info: logger.info,
warn: logger.warn,
error: logger.error,
debug: logger.debug,
});
const createApi = (
record: PluginRecord,
params: {
@ -529,21 +522,17 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
hookPolicy?: PluginTypedHookPolicy;
},
): OpenClawPluginApi => {
return {
id: record.id,
name: record.name,
version: record.version,
description: record.description,
source: record.source,
return createExtensionHostPluginApi({
record,
runtime: registryParams.runtime,
logger: registryParams.logger,
config: params.config,
pluginConfig: params.pluginConfig,
runtime: registryParams.runtime,
logger: normalizeLogger(registryParams.logger),
registerTool: (tool, opts) => registerTool(record, tool, opts),
registerHook: (events, handler, opts) =>
registerHook(record, events, handler, opts, params.config),
registerHttpRoute: (params) => registerHttpRoute(record, params),
registerChannel: (registration) => registerChannel(record, registration),
registerHttpRoute: (routeParams) => registerHttpRoute(record, routeParams),
registerChannel: (registration) => registerChannel(record, registration as never),
registerProvider: (provider) => registerProvider(record, provider),
registerGatewayMethod: (method, handler) => registerGatewayMethod(record, method, handler),
registerCli: (registrar, opts) => registerCli(record, registrar, opts),
@ -568,10 +557,9 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
registerEngine: registerContextEngine,
});
},
resolvePath: (input: string) => resolveUserPath(input),
on: (hookName, handler, opts) =>
registerTypedHook(record, hookName, handler, opts, params.hookPolicy),
};
});
};
return {