Plugins: add host-owned tool and provider storage

This commit is contained in:
Gustavo Madeira Santana 2026-03-15 18:43:25 +00:00
parent d34a5aa870
commit 2be54e9861
No known key found for this signature in database
7 changed files with 170 additions and 26 deletions

View File

@ -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,

View File

@ -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: {

View File

@ -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", () => {

View File

@ -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: {

View File

@ -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);
});
});

View File

@ -17,6 +17,10 @@ const EMPTY_GATEWAY_HANDLERS: Readonly<GatewayRequestHandlers> = 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<PluginRegistry, "providers"> | 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<PluginRegistry, "tools"> | 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<PluginRegistry, "httpRoutes" | "gatewayHandlers"> | 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<PluginRegistry, "httpRoutes" | "gatewayHandlers"> | null | undefined,
registry:
| Pick<
PluginRegistry,
"tools" | "providers" | "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers"
>
| null
| undefined,
): Readonly<GatewayRequestHandlers> {
if (!registry) {
return EMPTY_GATEWAY_HANDLERS;
@ -181,7 +233,10 @@ export function getExtensionHostGatewayHandlers(
}
export function addExtensionHostHttpRoute(
registry: Pick<PluginRegistry, "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers">,
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<PluginRegistry, "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers">;
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<PluginRegistry, "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers">,
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<PluginRegistry, "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers">;
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<PluginRegistry, "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers">,
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<PluginRegistry, "cliRegistrars" | "services" | "httpRoutes" | "gatewayHandlers">,
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);
}

View File

@ -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",