From 2be54e98612ab7ff15b20a067091576d3ccdbb78 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Sun, 15 Mar 2026 18:43:25 +0000 Subject: [PATCH] Plugins: add host-owned tool and provider storage --- src/extension-host/plugin-registry-compat.ts | 3 +- src/extension-host/provider-runtime.test.ts | 3 +- src/extension-host/registry-writes.test.ts | 1 + src/extension-host/registry-writes.ts | 6 +- src/extension-host/runtime-registry.test.ts | 46 ++++++- src/extension-host/runtime-registry.ts | 128 ++++++++++++++++--- src/extension-host/tool-runtime.test.ts | 9 +- 7 files changed, 170 insertions(+), 26 deletions(-) diff --git a/src/extension-host/plugin-registry-compat.ts b/src/extension-host/plugin-registry-compat.ts index 5a794ad59db..1a68791a5d4 100644 --- a/src/extension-host/plugin-registry-compat.ts +++ b/src/extension-host/plugin-registry-compat.ts @@ -12,6 +12,7 @@ import { resolveExtensionCommandRegistration, resolveExtensionProviderRegistration, } from "./runtime-registrations.js"; +import { listExtensionHostProviderRegistrations } from "./runtime-registry.js"; export function pushExtensionHostRegistryDiagnostic(params: { registry: PluginRegistry; @@ -54,7 +55,7 @@ export function resolveExtensionHostProviderCompatibility(params: { } const result = resolveExtensionProviderRegistration({ - existing: params.registry.providers, + existing: [...listExtensionHostProviderRegistrations(params.registry)], ownerPluginId: params.record.id, ownerSource: params.record.source, provider: normalizedProvider, diff --git a/src/extension-host/provider-runtime.test.ts b/src/extension-host/provider-runtime.test.ts index 224452005cb..1ad3f211efa 100644 --- a/src/extension-host/provider-runtime.test.ts +++ b/src/extension-host/provider-runtime.test.ts @@ -1,11 +1,12 @@ import { describe, expect, it } from "vitest"; import { createEmptyPluginRegistry } from "../plugins/registry.js"; import { resolveExtensionHostProviders } from "./provider-runtime.js"; +import { addExtensionHostProviderRegistration } from "./runtime-registry.js"; describe("resolveExtensionHostProviders", () => { it("projects provider registrations into provider plugins with plugin ids", () => { const registry = createEmptyPluginRegistry(); - registry.providers.push({ + addExtensionHostProviderRegistration(registry, { pluginId: "demo-plugin", source: "bundled", provider: { diff --git a/src/extension-host/registry-writes.test.ts b/src/extension-host/registry-writes.test.ts index f8ba5154da7..f7877504e52 100644 --- a/src/extension-host/registry-writes.test.ts +++ b/src/extension-host/registry-writes.test.ts @@ -154,6 +154,7 @@ describe("extension host registry writes", () => { expect(registry.httpRoutes).toHaveLength(1); expect(registry.channels).toHaveLength(1); expect(registry.providers).toHaveLength(1); + expect(registry.providers[0]?.pluginId).toBe("demo"); }); it("writes legacy hooks, typed hooks, and context engines through host helpers", () => { diff --git a/src/extension-host/registry-writes.ts b/src/extension-host/registry-writes.ts index 1b363aaaaf0..fbf3a27a0a5 100644 --- a/src/extension-host/registry-writes.ts +++ b/src/extension-host/registry-writes.ts @@ -27,7 +27,9 @@ import type { import { addExtensionHostCliRegistration, addExtensionHostHttpRoute, + addExtensionHostProviderRegistration, addExtensionHostServiceRegistration, + addExtensionHostToolRegistration, replaceExtensionHostHttpRoute, setExtensionHostGatewayHandler, } from "./runtime-registry.js"; @@ -86,7 +88,7 @@ export function addExtensionProviderRegistration(params: { entry: ExtensionHostProviderRegistration; }): void { params.record.providerIds.push(params.providerId); - params.registry.providers.push(params.entry as PluginProviderRegistration); + addExtensionHostProviderRegistration(params.registry, params.entry as PluginProviderRegistration); } export function addExtensionLegacyHookRegistration(params: { @@ -123,7 +125,7 @@ export function addExtensionToolRegistration(params: { if (params.names.length > 0) { params.record.toolNames.push(...params.names); } - params.registry.tools.push(params.entry as PluginToolRegistration); + addExtensionHostToolRegistration(params.registry, params.entry as PluginToolRegistration); } export function addExtensionCliRegistration(params: { diff --git a/src/extension-host/runtime-registry.test.ts b/src/extension-host/runtime-registry.test.ts index bfc78418176..7f0094dd096 100644 --- a/src/extension-host/runtime-registry.test.ts +++ b/src/extension-host/runtime-registry.test.ts @@ -3,7 +3,9 @@ import { createEmptyPluginRegistry } from "../plugins/registry.js"; import { addExtensionHostCliRegistration, addExtensionHostHttpRoute, + addExtensionHostProviderRegistration, addExtensionHostServiceRegistration, + addExtensionHostToolRegistration, getExtensionHostGatewayHandlers, hasExtensionHostRuntimeEntries, listExtensionHostCliRegistrations, @@ -19,7 +21,7 @@ import { describe("extension host runtime registry accessors", () => { it("detects runtime entries across non-tool surfaces", () => { const providerRegistry = createEmptyPluginRegistry(); - providerRegistry.providers.push({ + addExtensionHostProviderRegistration(providerRegistry, { pluginId: "provider-demo", source: "test", provider: { @@ -82,7 +84,7 @@ describe("extension host runtime registry accessors", () => { it("projects existing registry collections without copying them", () => { const registry = createEmptyPluginRegistry(); - registry.tools.push({ + addExtensionHostToolRegistration(registry, { pluginId: "tool-demo", optional: false, source: "test", @@ -96,6 +98,15 @@ describe("extension host runtime registry accessors", () => { }, }), }); + addExtensionHostProviderRegistration(registry, { + pluginId: "provider-demo", + source: "test", + provider: { + id: "provider-demo", + label: "Provider Demo", + auth: [], + }, + }); addExtensionHostServiceRegistration(registry, { pluginId: "svc-demo", source: "test", @@ -125,7 +136,8 @@ describe("extension host runtime registry accessors", () => { handler, }); - expect(listExtensionHostToolRegistrations(registry)).toBe(registry.tools); + expect(listExtensionHostToolRegistrations(registry)).toEqual(registry.tools); + expect(listExtensionHostProviderRegistrations(registry)).toEqual(registry.providers); expect(listExtensionHostServiceRegistrations(registry)).toEqual(registry.services); expect(listExtensionHostCliRegistrations(registry)).toEqual(registry.cliRegistrars); expect(listExtensionHostHttpRoutes(registry)).toEqual(registry.httpRoutes); @@ -189,4 +201,32 @@ describe("extension host runtime registry accessors", () => { expect(registry.services[0]?.service).toBe(service); expect(registry.cliRegistrars[0]?.register).toBe(register); }); + + it("keeps legacy tool and provider mirrors synchronized with host-owned state", () => { + const registry = createEmptyPluginRegistry(); + const factory = (() => ({}) as never) as never; + const provider = { + id: "provider-demo", + label: "Provider Demo", + auth: [], + }; + + addExtensionHostToolRegistration(registry, { + pluginId: "tool-demo", + optional: false, + source: "test", + names: ["tool_demo"], + factory, + }); + addExtensionHostProviderRegistration(registry, { + pluginId: "provider-demo", + source: "test", + provider, + }); + + expect(listExtensionHostToolRegistrations(registry)).toEqual(registry.tools); + expect(listExtensionHostProviderRegistrations(registry)).toEqual(registry.providers); + expect(registry.tools[0]?.factory).toBe(factory); + expect(registry.providers[0]?.provider).toBe(provider); + }); }); diff --git a/src/extension-host/runtime-registry.ts b/src/extension-host/runtime-registry.ts index c5ce57ed2ba..15a914fb3ba 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 = { + tools: PluginToolRegistration[]; + legacyTools: PluginToolRegistration[]; + providers: PluginProviderRegistration[]; + legacyProviders: PluginProviderRegistration[]; cliRegistrars: PluginCliRegistration[]; legacyCliRegistrars: PluginCliRegistration[]; services: PluginServiceRegistration[]; @@ -29,7 +33,7 @@ type ExtensionHostRuntimeRegistryState = { type RuntimeRegistryBackedPluginRegistry = Pick< PluginRegistry, - "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers" + "tools" | "providers" | "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers" > & { [EXTENSION_HOST_RUNTIME_REGISTRY_STATE]?: ExtensionHostRuntimeRegistryState; }; @@ -49,8 +53,16 @@ function ensureExtensionHostRuntimeRegistryState( registry.cliRegistrars = legacyCliRegistrars; const legacyServices = registry.services ?? []; registry.services = legacyServices; + const legacyTools = registry.tools ?? []; + registry.tools = legacyTools; + const legacyProviders = registry.providers ?? []; + registry.providers = legacyProviders; const state: ExtensionHostRuntimeRegistryState = { + tools: [...legacyTools], + legacyTools, + providers: [...legacyProviders], + legacyProviders, cliRegistrars: [...legacyCliRegistrars], legacyCliRegistrars, services: [...legacyServices], @@ -64,6 +76,14 @@ function ensureExtensionHostRuntimeRegistryState( return state; } +function syncLegacyTools(state: ExtensionHostRuntimeRegistryState): void { + state.legacyTools.splice(0, state.legacyTools.length, ...state.tools); +} + +function syncLegacyProviders(state: ExtensionHostRuntimeRegistryState): void { + state.legacyProviders.splice(0, state.legacyProviders.length, ...state.providers); +} + function syncLegacyCliRegistrars(state: ExtensionHostRuntimeRegistryState): void { state.legacyCliRegistrars.splice(0, state.legacyCliRegistrars.length, ...state.cliRegistrars); } @@ -110,8 +130,8 @@ export function hasExtensionHostRuntimeEntries( return ( registry.plugins.length > 0 || registry.channels.length > 0 || - registry.tools.length > 0 || - registry.providers.length > 0 || + listExtensionHostToolRegistrations(registry).length > 0 || + listExtensionHostProviderRegistrations(registry).length > 0 || Object.keys(getExtensionHostGatewayHandlers(registry)).length > 0 || listExtensionHostHttpRoutes(registry).length > 0 || listExtensionHostCliRegistrations(registry).length > 0 || @@ -123,15 +143,35 @@ export function hasExtensionHostRuntimeEntries( } export function listExtensionHostProviderRegistrations( - registry: Pick | null | undefined, + registry: + | Pick< + PluginRegistry, + "tools" | "providers" | "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers" + > + | null + | undefined, ): readonly PluginProviderRegistration[] { - return registry?.providers ?? EMPTY_PROVIDERS; + if (!registry) { + return EMPTY_PROVIDERS; + } + return ensureExtensionHostRuntimeRegistryState(registry as RuntimeRegistryBackedPluginRegistry) + .providers; } export function listExtensionHostToolRegistrations( - registry: Pick | null | undefined, + registry: + | Pick< + PluginRegistry, + "tools" | "providers" | "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers" + > + | null + | undefined, ): readonly PluginToolRegistration[] { - return registry?.tools ?? EMPTY_TOOLS; + if (!registry) { + return EMPTY_TOOLS; + } + return ensureExtensionHostRuntimeRegistryState(registry as RuntimeRegistryBackedPluginRegistry) + .tools; } export function listExtensionHostServiceRegistrations( @@ -161,7 +201,13 @@ export function listExtensionHostCliRegistrations( } export function listExtensionHostHttpRoutes( - registry: Pick | null | undefined, + registry: + | Pick< + PluginRegistry, + "tools" | "providers" | "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers" + > + | null + | undefined, ): readonly PluginHttpRouteRegistration[] { if (!registry) { return EMPTY_HTTP_ROUTES; @@ -171,7 +217,13 @@ export function listExtensionHostHttpRoutes( } export function getExtensionHostGatewayHandlers( - registry: Pick | null | undefined, + registry: + | Pick< + PluginRegistry, + "tools" | "providers" | "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers" + > + | null + | undefined, ): Readonly { if (!registry) { return EMPTY_GATEWAY_HANDLERS; @@ -181,7 +233,10 @@ export function getExtensionHostGatewayHandlers( } export function addExtensionHostHttpRoute( - registry: Pick, + registry: Pick< + PluginRegistry, + "tools" | "providers" | "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers" + >, entry: PluginHttpRouteRegistration, ): void { const state = ensureExtensionHostRuntimeRegistryState( @@ -192,7 +247,10 @@ export function addExtensionHostHttpRoute( } export function replaceExtensionHostHttpRoute(params: { - registry: Pick; + registry: Pick< + PluginRegistry, + "tools" | "providers" | "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers" + >; index: number; entry: PluginHttpRouteRegistration; }): void { @@ -204,7 +262,10 @@ export function replaceExtensionHostHttpRoute(params: { } export function removeExtensionHostHttpRoute( - registry: Pick, + registry: Pick< + PluginRegistry, + "tools" | "providers" | "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers" + >, entry: PluginHttpRouteRegistration, ): void { const state = ensureExtensionHostRuntimeRegistryState( @@ -219,7 +280,10 @@ export function removeExtensionHostHttpRoute( } export function setExtensionHostGatewayHandler(params: { - registry: Pick; + registry: Pick< + PluginRegistry, + "tools" | "providers" | "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers" + >; method: string; handler: GatewayRequestHandlers[string]; }): void { @@ -231,7 +295,10 @@ export function setExtensionHostGatewayHandler(params: { } export function addExtensionHostCliRegistration( - registry: Pick, + registry: Pick< + PluginRegistry, + "tools" | "providers" | "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers" + >, entry: PluginCliRegistration, ): void { const state = ensureExtensionHostRuntimeRegistryState( @@ -242,7 +309,10 @@ export function addExtensionHostCliRegistration( } export function addExtensionHostServiceRegistration( - registry: Pick, + registry: Pick< + PluginRegistry, + "tools" | "providers" | "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers" + >, entry: PluginServiceRegistration, ): void { const state = ensureExtensionHostRuntimeRegistryState( @@ -251,3 +321,31 @@ export function addExtensionHostServiceRegistration( state.services.push(entry); syncLegacyServices(state); } + +export function addExtensionHostToolRegistration( + registry: Pick< + PluginRegistry, + "tools" | "providers" | "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers" + >, + entry: PluginToolRegistration, +): void { + const state = ensureExtensionHostRuntimeRegistryState( + registry as RuntimeRegistryBackedPluginRegistry, + ); + state.tools.push(entry); + syncLegacyTools(state); +} + +export function addExtensionHostProviderRegistration( + registry: Pick< + PluginRegistry, + "tools" | "providers" | "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers" + >, + entry: PluginProviderRegistration, +): void { + const state = ensureExtensionHostRuntimeRegistryState( + registry as RuntimeRegistryBackedPluginRegistry, + ); + state.providers.push(entry); + syncLegacyProviders(state); +} diff --git a/src/extension-host/tool-runtime.test.ts b/src/extension-host/tool-runtime.test.ts index b74d15d7be7..cd6af8eeb66 100644 --- a/src/extension-host/tool-runtime.test.ts +++ b/src/extension-host/tool-runtime.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import type { AnyAgentTool } from "../agents/tools/common.js"; import { createEmptyPluginRegistry } from "../plugins/registry.js"; +import { addExtensionHostToolRegistration } from "./runtime-registry.js"; import { getExtensionHostPluginToolMeta, resolveExtensionHostPluginTools } from "./tool-runtime.js"; function makeTool(name: string): AnyAgentTool { @@ -28,7 +29,7 @@ function createContext() { describe("resolveExtensionHostPluginTools", () => { it("allows optional tools through tool, plugin, and plugin-group allowlists", () => { const registry = createEmptyPluginRegistry(); - registry.tools.push({ + addExtensionHostToolRegistration(registry, { pluginId: "optional-demo", optional: true, source: "/tmp/optional-demo.js", @@ -68,14 +69,14 @@ describe("resolveExtensionHostPluginTools", () => { it("records conflict diagnostics and preserves tool metadata", () => { const registry = createEmptyPluginRegistry(); const extraTool = makeTool("other_tool"); - registry.tools.push({ + addExtensionHostToolRegistration(registry, { pluginId: "message", optional: false, source: "/tmp/message.js", factory: () => makeTool("optional_tool"), names: ["optional_tool"], }); - registry.tools.push({ + addExtensionHostToolRegistration(registry, { pluginId: "multi", optional: false, source: "/tmp/multi.js", @@ -104,7 +105,7 @@ describe("resolveExtensionHostPluginTools", () => { const factory = vi.fn(() => { throw new Error("boom"); }); - registry.tools.push({ + addExtensionHostToolRegistration(registry, { pluginId: "broken", optional: false, source: "/tmp/broken.js",