diff --git a/src/extension-host/cutover-inventory.md b/src/extension-host/cutover-inventory.md index b60392bae0a..0509900e001 100644 --- a/src/extension-host/cutover-inventory.md +++ b/src/extension-host/cutover-inventory.md @@ -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`. | diff --git a/src/extension-host/plugin-api.test.ts b/src/extension-host/plugin-api.test.ts new file mode 100644 index 00000000000..d05fbc1190d --- /dev/null +++ b/src/extension-host/plugin-api.test.ts @@ -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); + }); +}); diff --git a/src/extension-host/plugin-api.ts b/src/extension-host/plugin-api.ts new file mode 100644 index 00000000000..dff59fd3cf6 --- /dev/null +++ b/src/extension-host/plugin-api.ts @@ -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; + registerTool: ( + tool: OpenClawPluginToolFactory | { name: string }, + opts?: { name?: string; names?: string[]; optional?: boolean }, + ) => void; + registerHook: ( + events: string | string[], + handler: Parameters[1], + opts?: Parameters[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[1], + ) => void; + on: ( + 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), + }; +} diff --git a/src/plugins/registry.ts b/src/plugins/registry.ts index 18e0bfb77c2..5add533ed78 100644 --- a/src/plugins/registry.ts +++ b/src/plugins/registry.ts @@ -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 {