Plugins: extract plugin api facade

This commit is contained in:
Gustavo Madeira Santana 2026-03-15 17:04:07 +00:00
parent cacd8898b2
commit 639178bd16
No known key found for this signature in database
4 changed files with 196 additions and 14 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

@ -7,7 +7,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 { normalizePluginHttpPath } from "./http-path.js";
import { findOverlappingPluginHttpRoute } from "./http-route-overlap.js";
@ -641,13 +640,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: {
@ -665,13 +657,11 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
rootDir: record.rootDir,
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),
@ -713,10 +703,9 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
});
}
},
resolvePath: (input: string) => resolveUserPath(input),
on: (hookName, handler, opts) =>
registerTypedHook(record, hookName, handler, opts, params.hookPolicy),
};
});
};
return {