Plugins: extract registry compatibility policy

This commit is contained in:
Gustavo Madeira Santana 2026-03-15 17:14:06 +00:00
parent 33ab19e61e
commit 944d787df1
No known key found for this signature in database
3 changed files with 217 additions and 65 deletions

View File

@ -0,0 +1,92 @@
import { describe, expect, it, vi } from "vitest";
import { clearPluginCommands } from "../plugins/commands.js";
import { createEmptyPluginRegistry, type PluginRecord } from "../plugins/registry.js";
import {
resolveExtensionHostCommandCompatibility,
resolveExtensionHostProviderCompatibility,
} from "./plugin-registry-compat.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 registry compatibility", () => {
it("normalizes provider registration through the host-owned compatibility helper", () => {
const result = resolveExtensionHostProviderCompatibility({
registry: createEmptyPluginRegistry(),
record: createRecord(),
provider: {
id: " demo-provider ",
label: " Demo Provider ",
auth: [{ id: " api-key ", label: " API Key " }],
} as never,
});
expect(result).toMatchObject({
ok: true,
providerId: "demo-provider",
entry: {
provider: {
id: "demo-provider",
label: "Demo Provider",
auth: [{ id: "api-key", label: "API Key" }],
},
},
});
});
it("reports duplicate command registration through the host-owned compatibility helper", () => {
clearPluginCommands();
const registry = createEmptyPluginRegistry();
const record = createRecord();
const first = resolveExtensionHostCommandCompatibility({
registry,
record,
command: {
name: "demo",
description: "first",
handler: vi.fn(async () => ({ handled: true })),
},
});
const second = resolveExtensionHostCommandCompatibility({
registry,
record,
command: {
name: "demo",
description: "second",
handler: vi.fn(async () => ({ handled: true })),
},
});
expect(first.ok).toBe(true);
expect(second.ok).toBe(false);
expect(registry.diagnostics).toContainEqual(
expect.objectContaining({
level: "error",
pluginId: "demo",
message: 'command registration failed: Command "demo" already registered by plugin "demo"',
}),
);
clearPluginCommands();
});
});

View File

@ -0,0 +1,116 @@
import { registerPluginCommand } from "../plugins/commands.js";
import { normalizeRegisteredProvider } from "../plugins/provider-validation.js";
import type { PluginRecord, PluginRegistry } from "../plugins/registry.js";
import type {
OpenClawPluginCommandDefinition,
PluginDiagnostic,
ProviderPlugin,
} from "../plugins/types.js";
import {
type ExtensionHostCommandRegistration,
type ExtensionHostProviderRegistration,
resolveExtensionCommandRegistration,
resolveExtensionProviderRegistration,
} from "./runtime-registrations.js";
export function pushExtensionHostRegistryDiagnostic(params: {
registry: PluginRegistry;
level: PluginDiagnostic["level"];
pluginId: string;
source: string;
message: string;
}) {
params.registry.diagnostics.push({
level: params.level,
pluginId: params.pluginId,
source: params.source,
message: params.message,
});
}
export function resolveExtensionHostProviderCompatibility(params: {
registry: PluginRegistry;
record: PluginRecord;
provider: ProviderPlugin;
}):
| {
ok: true;
providerId: string;
entry: ExtensionHostProviderRegistration;
}
| { ok: false } {
const pushDiagnostic = (diag: PluginDiagnostic) => {
params.registry.diagnostics.push(diag);
};
const normalizedProvider = normalizeRegisteredProvider({
pluginId: params.record.id,
source: params.record.source,
provider: params.provider,
pushDiagnostic,
});
if (!normalizedProvider) {
return { ok: false };
}
const result = resolveExtensionProviderRegistration({
existing: params.registry.providers,
ownerPluginId: params.record.id,
ownerSource: params.record.source,
provider: normalizedProvider,
});
if (!result.ok) {
pushExtensionHostRegistryDiagnostic({
registry: params.registry,
level: "error",
pluginId: params.record.id,
source: params.record.source,
message: result.message,
});
return { ok: false };
}
return result;
}
export function resolveExtensionHostCommandCompatibility(params: {
registry: PluginRegistry;
record: PluginRecord;
command: OpenClawPluginCommandDefinition;
}):
| {
ok: true;
commandName: string;
entry: ExtensionHostCommandRegistration;
}
| { ok: false } {
const normalized = resolveExtensionCommandRegistration({
ownerPluginId: params.record.id,
ownerSource: params.record.source,
command: params.command,
});
if (!normalized.ok) {
pushExtensionHostRegistryDiagnostic({
registry: params.registry,
level: "error",
pluginId: params.record.id,
source: params.record.source,
message: normalized.message,
});
return { ok: false };
}
const result = registerPluginCommand(params.record.id, normalized.entry.command);
if (!result.ok) {
pushExtensionHostRegistryDiagnostic({
registry: params.registry,
level: "error",
pluginId: params.record.id,
source: params.record.source,
message: `command registration failed: ${result.error}`,
});
return { ok: false };
}
return normalized;
}

View File

@ -22,20 +22,16 @@ import {
import {
resolveExtensionChannelRegistration,
resolveExtensionCliRegistration,
resolveExtensionCommandRegistration,
resolveExtensionContextEngineRegistration,
resolveExtensionGatewayMethodRegistration,
resolveExtensionLegacyHookRegistration,
resolveExtensionHttpRouteRegistration,
resolveExtensionProviderRegistration,
resolveExtensionServiceRegistration,
resolveExtensionToolRegistration,
resolveExtensionTypedHookRegistration,
} from "../extension-host/runtime-registrations.js";
import type { GatewayRequestHandler } from "../gateway/server-methods/types.js";
import { registerInternalHook } from "../hooks/internal-hooks.js";
import { registerPluginCommand } from "../plugins/commands.js";
import { normalizeRegisteredProvider } from "../plugins/provider-validation.js";
import type { PluginRecord, PluginRegistry, PluginRegistryParams } from "../plugins/registry.js";
import type {
PluginDiagnostic,
@ -52,33 +48,22 @@ import type {
ProviderPlugin,
PluginHookRegistration as TypedPluginHookRegistration,
} from "../plugins/types.js";
import {
pushExtensionHostRegistryDiagnostic,
resolveExtensionHostCommandCompatibility,
resolveExtensionHostProviderCompatibility,
} from "./plugin-registry-compat.js";
type PluginTypedHookPolicy = {
allowPromptInjection?: boolean;
};
function pushExtensionHostRegistryDiagnostic(params: {
registry: PluginRegistry;
level: PluginDiagnostic["level"];
pluginId: string;
source: string;
message: string;
}) {
params.registry.diagnostics.push({
level: params.level,
pluginId: params.pluginId,
source: params.source,
message: params.message,
});
}
export function createExtensionHostPluginRegistry(params: {
registry: PluginRegistry;
registryParams: PluginRegistryParams;
}) {
const { registry, registryParams } = params;
const coreGatewayMethods = new Set(Object.keys(registryParams.coreGatewayHandlers ?? {}));
const pushDiagnostic = (diag: PluginDiagnostic) => {
registry.diagnostics.push(diag);
};
@ -231,29 +216,12 @@ export function createExtensionHostPluginRegistry(params: {
};
const registerProvider = (record: PluginRecord, provider: ProviderPlugin) => {
const normalizedProvider = normalizeRegisteredProvider({
pluginId: record.id,
source: record.source,
const result = resolveExtensionHostProviderCompatibility({
registry,
record,
provider,
pushDiagnostic,
});
if (!normalizedProvider) {
return;
}
const result = resolveExtensionProviderRegistration({
existing: registry.providers,
ownerPluginId: record.id,
ownerSource: record.source,
provider: normalizedProvider,
});
if (!result.ok) {
pushExtensionHostRegistryDiagnostic({
registry,
level: "error",
pluginId: record.id,
source: record.source,
message: result.message,
});
return;
}
addExtensionProviderRegistration({
@ -301,34 +269,10 @@ export function createExtensionHostPluginRegistry(params: {
};
const registerCommand = (record: PluginRecord, command: OpenClawPluginCommandDefinition) => {
const normalized = resolveExtensionCommandRegistration({
ownerPluginId: record.id,
ownerSource: record.source,
command,
});
const normalized = resolveExtensionHostCommandCompatibility({ registry, record, command });
if (!normalized.ok) {
pushExtensionHostRegistryDiagnostic({
registry,
level: "error",
pluginId: record.id,
source: record.source,
message: normalized.message,
});
return;
}
const result = registerPluginCommand(record.id, normalized.entry.command);
if (!result.ok) {
pushExtensionHostRegistryDiagnostic({
registry,
level: "error",
pluginId: record.id,
source: record.source,
message: `command registration failed: ${result.error}`,
});
return;
}
addExtensionCommandRegistration({
registry,
record,