From daad214a3a37fb4495e5ce13630e4335e847c633 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Sun, 15 Mar 2026 18:40:10 +0000 Subject: [PATCH] Plugins: add host-owned CLI and service storage --- src/extension-host/registry-writes.ts | 6 +- src/extension-host/runtime-registry.test.ts | 56 +++++++++++++-- src/extension-host/runtime-registry.ts | 78 ++++++++++++++++++--- 3 files changed, 123 insertions(+), 17 deletions(-) diff --git a/src/extension-host/registry-writes.ts b/src/extension-host/registry-writes.ts index d2b0ebb6940..1b363aaaaf0 100644 --- a/src/extension-host/registry-writes.ts +++ b/src/extension-host/registry-writes.ts @@ -25,7 +25,9 @@ import type { ExtensionHostToolRegistration, } from "./runtime-registrations.js"; import { + addExtensionHostCliRegistration, addExtensionHostHttpRoute, + addExtensionHostServiceRegistration, replaceExtensionHostHttpRoute, setExtensionHostGatewayHandler, } from "./runtime-registry.js"; @@ -131,7 +133,7 @@ export function addExtensionCliRegistration(params: { entry: ExtensionHostCliRegistration; }): void { params.record.cliCommands.push(...params.commands); - params.registry.cliRegistrars.push(params.entry as PluginCliRegistration); + addExtensionHostCliRegistration(params.registry, params.entry as PluginCliRegistration); } export function addExtensionServiceRegistration(params: { @@ -141,7 +143,7 @@ export function addExtensionServiceRegistration(params: { entry: ExtensionHostServiceRegistration; }): void { params.record.services.push(params.serviceId); - params.registry.services.push(params.entry as PluginServiceRegistration); + addExtensionHostServiceRegistration(params.registry, params.entry as PluginServiceRegistration); } export function addExtensionCommandRegistration(params: { diff --git a/src/extension-host/runtime-registry.test.ts b/src/extension-host/runtime-registry.test.ts index 47ee4ecbde6..bfc78418176 100644 --- a/src/extension-host/runtime-registry.test.ts +++ b/src/extension-host/runtime-registry.test.ts @@ -1,7 +1,9 @@ import { describe, expect, it, vi } from "vitest"; import { createEmptyPluginRegistry } from "../plugins/registry.js"; import { + addExtensionHostCliRegistration, addExtensionHostHttpRoute, + addExtensionHostServiceRegistration, getExtensionHostGatewayHandlers, hasExtensionHostRuntimeEntries, listExtensionHostCliRegistrations, @@ -46,6 +48,26 @@ describe("extension host runtime registry accessors", () => { handler: vi.fn(), }); expect(hasExtensionHostRuntimeEntries(gatewayRegistry)).toBe(true); + + const cliRegistry = createEmptyPluginRegistry(); + addExtensionHostCliRegistration(cliRegistry, { + pluginId: "cli-demo", + source: "test", + commands: ["demo"], + register: () => undefined, + }); + expect(hasExtensionHostRuntimeEntries(cliRegistry)).toBe(true); + + const serviceRegistry = createEmptyPluginRegistry(); + addExtensionHostServiceRegistration(serviceRegistry, { + pluginId: "svc-demo", + source: "test", + service: { + id: "svc-demo", + start: () => undefined, + }, + }); + expect(hasExtensionHostRuntimeEntries(serviceRegistry)).toBe(true); }); it("returns stable empty views for missing registries", () => { @@ -74,7 +96,7 @@ describe("extension host runtime registry accessors", () => { }, }), }); - registry.services.push({ + addExtensionHostServiceRegistration(registry, { pluginId: "svc-demo", source: "test", service: { @@ -82,7 +104,7 @@ describe("extension host runtime registry accessors", () => { start: () => undefined, }, }); - registry.cliRegistrars.push({ + addExtensionHostCliRegistration(registry, { pluginId: "cli-demo", source: "test", commands: ["demo"], @@ -104,8 +126,8 @@ describe("extension host runtime registry accessors", () => { }); expect(listExtensionHostToolRegistrations(registry)).toBe(registry.tools); - expect(listExtensionHostServiceRegistrations(registry)).toBe(registry.services); - expect(listExtensionHostCliRegistrations(registry)).toBe(registry.cliRegistrars); + expect(listExtensionHostServiceRegistrations(registry)).toEqual(registry.services); + expect(listExtensionHostCliRegistrations(registry)).toEqual(registry.cliRegistrars); expect(listExtensionHostHttpRoutes(registry)).toEqual(registry.httpRoutes); expect(getExtensionHostGatewayHandlers(registry)).toEqual(registry.gatewayHandlers); expect(getExtensionHostGatewayHandlers(registry)["demo.echo"]).toBe(handler); @@ -141,4 +163,30 @@ describe("extension host runtime registry accessors", () => { expect(registry.httpRoutes[0]?.handler).toBe(secondHandler); expect(getExtensionHostGatewayHandlers(registry)).toEqual(registry.gatewayHandlers); }); + + it("keeps legacy CLI and service mirrors synchronized with host-owned state", () => { + const registry = createEmptyPluginRegistry(); + const service = { + id: "svc-demo", + start: () => undefined, + }; + const register = () => undefined; + + addExtensionHostServiceRegistration(registry, { + pluginId: "svc-demo", + source: "test", + service, + }); + addExtensionHostCliRegistration(registry, { + pluginId: "cli-demo", + source: "test", + commands: ["demo"], + register, + }); + + expect(listExtensionHostServiceRegistrations(registry)).toEqual(registry.services); + expect(listExtensionHostCliRegistrations(registry)).toEqual(registry.cliRegistrars); + expect(registry.services[0]?.service).toBe(service); + expect(registry.cliRegistrars[0]?.register).toBe(register); + }); }); diff --git a/src/extension-host/runtime-registry.ts b/src/extension-host/runtime-registry.ts index f18962e7962..c5ce57ed2ba 100644 --- a/src/extension-host/runtime-registry.ts +++ b/src/extension-host/runtime-registry.ts @@ -17,6 +17,10 @@ const EMPTY_GATEWAY_HANDLERS: Readonly = Object.freeze({ const EXTENSION_HOST_RUNTIME_REGISTRY_STATE = Symbol.for("openclaw.extensionHostRuntimeRegistry"); type ExtensionHostRuntimeRegistryState = { + cliRegistrars: PluginCliRegistration[]; + legacyCliRegistrars: PluginCliRegistration[]; + services: PluginServiceRegistration[]; + legacyServices: PluginServiceRegistration[]; httpRoutes: PluginHttpRouteRegistration[]; legacyHttpRoutes: PluginHttpRouteRegistration[]; gatewayHandlers: GatewayRequestHandlers; @@ -25,7 +29,7 @@ type ExtensionHostRuntimeRegistryState = { type RuntimeRegistryBackedPluginRegistry = Pick< PluginRegistry, - "httpRoutes" | "gatewayHandlers" + "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers" > & { [EXTENSION_HOST_RUNTIME_REGISTRY_STATE]?: ExtensionHostRuntimeRegistryState; }; @@ -41,8 +45,16 @@ function ensureExtensionHostRuntimeRegistryState( registry.httpRoutes = legacyHttpRoutes; const legacyGatewayHandlers = registry.gatewayHandlers ?? {}; registry.gatewayHandlers = legacyGatewayHandlers; + const legacyCliRegistrars = registry.cliRegistrars ?? []; + registry.cliRegistrars = legacyCliRegistrars; + const legacyServices = registry.services ?? []; + registry.services = legacyServices; const state: ExtensionHostRuntimeRegistryState = { + cliRegistrars: [...legacyCliRegistrars], + legacyCliRegistrars, + services: [...legacyServices], + legacyServices, httpRoutes: [...legacyHttpRoutes], legacyHttpRoutes, gatewayHandlers: { ...legacyGatewayHandlers }, @@ -52,6 +64,14 @@ function ensureExtensionHostRuntimeRegistryState( return state; } +function syncLegacyCliRegistrars(state: ExtensionHostRuntimeRegistryState): void { + state.legacyCliRegistrars.splice(0, state.legacyCliRegistrars.length, ...state.cliRegistrars); +} + +function syncLegacyServices(state: ExtensionHostRuntimeRegistryState): void { + state.legacyServices.splice(0, state.legacyServices.length, ...state.services); +} + function syncLegacyHttpRoutes(state: ExtensionHostRuntimeRegistryState): void { state.legacyHttpRoutes.splice(0, state.legacyHttpRoutes.length, ...state.httpRoutes); } @@ -94,8 +114,8 @@ export function hasExtensionHostRuntimeEntries( registry.providers.length > 0 || Object.keys(getExtensionHostGatewayHandlers(registry)).length > 0 || listExtensionHostHttpRoutes(registry).length > 0 || - registry.cliRegistrars.length > 0 || - registry.services.length > 0 || + listExtensionHostCliRegistrations(registry).length > 0 || + listExtensionHostServiceRegistrations(registry).length > 0 || registry.commands.length > 0 || registry.hooks.length > 0 || registry.typedHooks.length > 0 @@ -115,15 +135,29 @@ export function listExtensionHostToolRegistrations( } export function listExtensionHostServiceRegistrations( - registry: Pick | null | undefined, + registry: + | Pick + | null + | undefined, ): readonly PluginServiceRegistration[] { - return registry?.services ?? EMPTY_SERVICES; + if (!registry) { + return EMPTY_SERVICES; + } + return ensureExtensionHostRuntimeRegistryState(registry as RuntimeRegistryBackedPluginRegistry) + .services; } export function listExtensionHostCliRegistrations( - registry: Pick | null | undefined, + registry: + | Pick + | null + | undefined, ): readonly PluginCliRegistration[] { - return registry?.cliRegistrars ?? EMPTY_CLI_REGISTRARS; + if (!registry) { + return EMPTY_CLI_REGISTRARS; + } + return ensureExtensionHostRuntimeRegistryState(registry as RuntimeRegistryBackedPluginRegistry) + .cliRegistrars; } export function listExtensionHostHttpRoutes( @@ -147,7 +181,7 @@ export function getExtensionHostGatewayHandlers( } export function addExtensionHostHttpRoute( - registry: Pick, + registry: Pick, entry: PluginHttpRouteRegistration, ): void { const state = ensureExtensionHostRuntimeRegistryState( @@ -158,7 +192,7 @@ export function addExtensionHostHttpRoute( } export function replaceExtensionHostHttpRoute(params: { - registry: Pick; + registry: Pick; index: number; entry: PluginHttpRouteRegistration; }): void { @@ -170,7 +204,7 @@ export function replaceExtensionHostHttpRoute(params: { } export function removeExtensionHostHttpRoute( - registry: Pick, + registry: Pick, entry: PluginHttpRouteRegistration, ): void { const state = ensureExtensionHostRuntimeRegistryState( @@ -185,7 +219,7 @@ export function removeExtensionHostHttpRoute( } export function setExtensionHostGatewayHandler(params: { - registry: Pick; + registry: Pick; method: string; handler: GatewayRequestHandlers[string]; }): void { @@ -195,3 +229,25 @@ export function setExtensionHostGatewayHandler(params: { state.gatewayHandlers[params.method] = params.handler; syncLegacyGatewayHandlers(state); } + +export function addExtensionHostCliRegistration( + registry: Pick, + entry: PluginCliRegistration, +): void { + const state = ensureExtensionHostRuntimeRegistryState( + registry as RuntimeRegistryBackedPluginRegistry, + ); + state.cliRegistrars.push(entry); + syncLegacyCliRegistrars(state); +} + +export function addExtensionHostServiceRegistration( + registry: Pick, + entry: PluginServiceRegistration, +): void { + const state = ensureExtensionHostRuntimeRegistryState( + registry as RuntimeRegistryBackedPluginRegistry, + ); + state.services.push(entry); + syncLegacyServices(state); +}