Contracts: harden plugin registry loading
This commit is contained in:
parent
7ac23ae7c2
commit
4ac9024de9
@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { loadPluginManifestRegistry } from "../manifest-registry.js";
|
||||
import { resolvePluginWebSearchProviders } from "../web-search-providers.js";
|
||||
import {
|
||||
capabilityContractLoadError,
|
||||
imageGenerationProviderContractRegistry,
|
||||
mediaUnderstandingProviderContractRegistry,
|
||||
pluginRegistrationContractRegistry,
|
||||
@ -85,6 +86,11 @@ function findRegistrationForPlugin(pluginId: string) {
|
||||
}
|
||||
|
||||
describe("plugin contract registry", () => {
|
||||
it("loads bundled non-provider capability registries without import-time failure", () => {
|
||||
expect(capabilityContractLoadError).toBeUndefined();
|
||||
expect(pluginRegistrationContractRegistry.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("does not duplicate bundled provider ids", () => {
|
||||
const ids = providerContractRegistry.map((entry) => entry.provider.id);
|
||||
expect(ids).toEqual([...new Set(ids)]);
|
||||
|
||||
@ -1,17 +1,8 @@
|
||||
import anthropicPlugin from "../../../extensions/anthropic/index.js";
|
||||
import bravePlugin from "../../../extensions/brave/index.js";
|
||||
import elevenLabsPlugin from "../../../extensions/elevenlabs/index.js";
|
||||
import firecrawlPlugin from "../../../extensions/firecrawl/index.js";
|
||||
import googlePlugin from "../../../extensions/google/index.js";
|
||||
import microsoftPlugin from "../../../extensions/microsoft/index.js";
|
||||
import minimaxPlugin from "../../../extensions/minimax/index.js";
|
||||
import mistralPlugin from "../../../extensions/mistral/index.js";
|
||||
import moonshotPlugin from "../../../extensions/moonshot/index.js";
|
||||
import openAIPlugin from "../../../extensions/openai/index.js";
|
||||
import perplexityPlugin from "../../../extensions/perplexity/index.js";
|
||||
import xaiPlugin from "../../../extensions/xai/index.js";
|
||||
import zaiPlugin from "../../../extensions/zai/index.js";
|
||||
import { createCapturedPluginRegistration } from "../captured-registration.js";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import { withBundledPluginEnablementCompat } from "../bundled-compat.js";
|
||||
import { resolveBundledWebSearchPluginIds } from "../bundled-web-search.js";
|
||||
import { loadOpenClawPlugins } from "../loader.js";
|
||||
import { createPluginLoaderLogger } from "../logger.js";
|
||||
import { resolvePluginProviders } from "../providers.js";
|
||||
import type {
|
||||
ImageGenerationProviderPlugin,
|
||||
@ -21,11 +12,6 @@ import type {
|
||||
WebSearchProviderPlugin,
|
||||
} from "../types.js";
|
||||
|
||||
type RegistrablePlugin = {
|
||||
id: string;
|
||||
register: (api: ReturnType<typeof createCapturedPluginRegistration>["api"]) => void;
|
||||
};
|
||||
|
||||
type CapabilityContractEntry<T> = {
|
||||
pluginId: string;
|
||||
provider: T;
|
||||
@ -52,52 +38,30 @@ type PluginRegistrationContractEntry = {
|
||||
toolNames: string[];
|
||||
};
|
||||
|
||||
const bundledWebSearchPlugins: Array<RegistrablePlugin & { credentialValue: unknown }> = [
|
||||
{ ...bravePlugin, credentialValue: "BSA-test" },
|
||||
{ ...firecrawlPlugin, credentialValue: "fc-test" },
|
||||
{ ...googlePlugin, credentialValue: "AIza-test" },
|
||||
{ ...moonshotPlugin, credentialValue: "sk-test" },
|
||||
{ ...perplexityPlugin, credentialValue: "pplx-test" },
|
||||
{ ...xaiPlugin, credentialValue: "xai-test" },
|
||||
];
|
||||
const log = createSubsystemLogger("plugins");
|
||||
|
||||
const bundledSpeechPlugins: RegistrablePlugin[] = [elevenLabsPlugin, microsoftPlugin, openAIPlugin];
|
||||
const BUNDLED_WEB_SEARCH_CREDENTIAL_VALUES: Readonly<Record<string, unknown>> = {
|
||||
brave: "BSA-test",
|
||||
firecrawl: "fc-test",
|
||||
google: "AIza-test",
|
||||
moonshot: "sk-test",
|
||||
perplexity: "pplx-test",
|
||||
xai: "xai-test",
|
||||
};
|
||||
|
||||
const bundledMediaUnderstandingPlugins: RegistrablePlugin[] = [
|
||||
anthropicPlugin,
|
||||
googlePlugin,
|
||||
minimaxPlugin,
|
||||
mistralPlugin,
|
||||
moonshotPlugin,
|
||||
openAIPlugin,
|
||||
zaiPlugin,
|
||||
];
|
||||
const BUNDLED_SPEECH_PLUGIN_IDS = ["elevenlabs", "microsoft", "openai"] as const;
|
||||
const BUNDLED_MEDIA_UNDERSTANDING_PLUGIN_IDS = [
|
||||
"anthropic",
|
||||
"google",
|
||||
"minimax",
|
||||
"mistral",
|
||||
"moonshot",
|
||||
"openai",
|
||||
"zai",
|
||||
] as const;
|
||||
const BUNDLED_IMAGE_GENERATION_PLUGIN_IDS = ["google", "openai"] as const;
|
||||
|
||||
const bundledImageGenerationPlugins: RegistrablePlugin[] = [googlePlugin, openAIPlugin];
|
||||
|
||||
function captureRegistrations(plugin: RegistrablePlugin) {
|
||||
const captured = createCapturedPluginRegistration();
|
||||
plugin.register(captured.api);
|
||||
return captured;
|
||||
}
|
||||
|
||||
function buildCapabilityContractRegistry<T>(params: {
|
||||
plugins: RegistrablePlugin[];
|
||||
select: (captured: ReturnType<typeof createCapturedPluginRegistration>) => T[];
|
||||
}): CapabilityContractEntry<T>[] {
|
||||
return params.plugins.flatMap((plugin) => {
|
||||
const captured = captureRegistrations(plugin);
|
||||
return params.select(captured).map((provider) => ({
|
||||
pluginId: plugin.id,
|
||||
provider,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
export const providerContractRegistry: ProviderContractEntry[] = buildCapabilityContractRegistry({
|
||||
plugins: [],
|
||||
select: () => [],
|
||||
});
|
||||
export const providerContractRegistry: ProviderContractEntry[] = [];
|
||||
|
||||
export let providerContractLoadError: Error | undefined;
|
||||
|
||||
@ -143,6 +107,55 @@ export const providerContractCompatPluginIds = providerContractPluginIds.map((pl
|
||||
pluginId === "kimi-coding" ? "kimi" : pluginId,
|
||||
);
|
||||
|
||||
const bundledCapabilityContractPluginIds = [
|
||||
...new Set([
|
||||
...providerContractCompatPluginIds,
|
||||
...resolveBundledWebSearchPluginIds({}),
|
||||
...BUNDLED_SPEECH_PLUGIN_IDS,
|
||||
...BUNDLED_MEDIA_UNDERSTANDING_PLUGIN_IDS,
|
||||
...BUNDLED_IMAGE_GENERATION_PLUGIN_IDS,
|
||||
]),
|
||||
].toSorted((left, right) => left.localeCompare(right));
|
||||
|
||||
export let capabilityContractLoadError: Error | undefined;
|
||||
|
||||
function loadBundledCapabilityRegistry() {
|
||||
try {
|
||||
capabilityContractLoadError = undefined;
|
||||
return loadOpenClawPlugins({
|
||||
config: withBundledPluginEnablementCompat({
|
||||
config: {
|
||||
plugins: {
|
||||
enabled: true,
|
||||
allow: bundledCapabilityContractPluginIds,
|
||||
slots: {
|
||||
memory: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
pluginIds: bundledCapabilityContractPluginIds,
|
||||
}),
|
||||
cache: false,
|
||||
activate: false,
|
||||
logger: createPluginLoaderLogger(log),
|
||||
});
|
||||
} catch (error) {
|
||||
capabilityContractLoadError = error instanceof Error ? error : new Error(String(error));
|
||||
return loadOpenClawPlugins({
|
||||
config: {
|
||||
plugins: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
cache: false,
|
||||
activate: false,
|
||||
logger: createPluginLoaderLogger(log),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const loadedBundledCapabilityRegistry = loadBundledCapabilityRegistry();
|
||||
|
||||
export function requireProviderContractProvider(providerId: string): ProviderPlugin {
|
||||
const provider = uniqueProviderContractProviders.find((entry) => entry.id === providerId);
|
||||
if (!provider) {
|
||||
@ -183,85 +196,50 @@ export function resolveProviderContractProvidersForPluginIds(
|
||||
}
|
||||
|
||||
export const webSearchProviderContractRegistry: WebSearchProviderContractEntry[] =
|
||||
bundledWebSearchPlugins.flatMap((plugin) => {
|
||||
const captured = captureRegistrations(plugin);
|
||||
return captured.webSearchProviders.map((provider) => ({
|
||||
pluginId: plugin.id,
|
||||
provider,
|
||||
credentialValue: plugin.credentialValue,
|
||||
loadedBundledCapabilityRegistry.webSearchProviders
|
||||
.filter((entry) => entry.pluginId in BUNDLED_WEB_SEARCH_CREDENTIAL_VALUES)
|
||||
.map((entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
provider: entry.provider,
|
||||
credentialValue: BUNDLED_WEB_SEARCH_CREDENTIAL_VALUES[entry.pluginId],
|
||||
}));
|
||||
});
|
||||
|
||||
export const speechProviderContractRegistry: SpeechProviderContractEntry[] =
|
||||
buildCapabilityContractRegistry({
|
||||
plugins: bundledSpeechPlugins,
|
||||
select: (captured) => captured.speechProviders,
|
||||
});
|
||||
loadedBundledCapabilityRegistry.speechProviders.map((entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
provider: entry.provider,
|
||||
}));
|
||||
|
||||
export const mediaUnderstandingProviderContractRegistry: MediaUnderstandingProviderContractEntry[] =
|
||||
buildCapabilityContractRegistry({
|
||||
plugins: bundledMediaUnderstandingPlugins,
|
||||
select: (captured) => captured.mediaUnderstandingProviders,
|
||||
});
|
||||
loadedBundledCapabilityRegistry.mediaUnderstandingProviders.map((entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
provider: entry.provider,
|
||||
}));
|
||||
|
||||
export const imageGenerationProviderContractRegistry: ImageGenerationProviderContractEntry[] =
|
||||
buildCapabilityContractRegistry({
|
||||
plugins: bundledImageGenerationPlugins,
|
||||
select: (captured) => captured.imageGenerationProviders,
|
||||
});
|
||||
loadedBundledCapabilityRegistry.imageGenerationProviders.map((entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
provider: entry.provider,
|
||||
}));
|
||||
|
||||
const bundledPluginRegistrationList = [
|
||||
...new Map(
|
||||
[
|
||||
...bundledSpeechPlugins,
|
||||
...bundledMediaUnderstandingPlugins,
|
||||
...bundledImageGenerationPlugins,
|
||||
...bundledWebSearchPlugins,
|
||||
].map((plugin) => [plugin.id, plugin]),
|
||||
).values(),
|
||||
];
|
||||
|
||||
export const pluginRegistrationContractRegistry: PluginRegistrationContractEntry[] = [
|
||||
...new Map(
|
||||
providerContractRegistry.map((entry) => [
|
||||
entry.pluginId,
|
||||
{
|
||||
pluginId: entry.pluginId,
|
||||
providerIds: providerContractRegistry
|
||||
.filter((candidate) => candidate.pluginId === entry.pluginId)
|
||||
.map((candidate) => candidate.provider.id),
|
||||
speechProviderIds: [] as string[],
|
||||
mediaUnderstandingProviderIds: [] as string[],
|
||||
imageGenerationProviderIds: [] as string[],
|
||||
webSearchProviderIds: [] as string[],
|
||||
toolNames: [] as string[],
|
||||
},
|
||||
]),
|
||||
).values(),
|
||||
];
|
||||
|
||||
for (const plugin of bundledPluginRegistrationList) {
|
||||
const captured = captureRegistrations(plugin);
|
||||
const existing = pluginRegistrationContractRegistry.find((entry) => entry.pluginId === plugin.id);
|
||||
const next = {
|
||||
pluginId: plugin.id,
|
||||
providerIds: captured.providers.map((provider) => provider.id),
|
||||
speechProviderIds: captured.speechProviders.map((provider) => provider.id),
|
||||
mediaUnderstandingProviderIds: captured.mediaUnderstandingProviders.map(
|
||||
(provider) => provider.id,
|
||||
),
|
||||
imageGenerationProviderIds: captured.imageGenerationProviders.map((provider) => provider.id),
|
||||
webSearchProviderIds: captured.webSearchProviders.map((provider) => provider.id),
|
||||
toolNames: captured.tools.map((tool) => tool.name),
|
||||
};
|
||||
if (!existing) {
|
||||
pluginRegistrationContractRegistry.push(next);
|
||||
continue;
|
||||
}
|
||||
existing.providerIds = next.providerIds.length > 0 ? next.providerIds : existing.providerIds;
|
||||
existing.speechProviderIds = next.speechProviderIds;
|
||||
existing.mediaUnderstandingProviderIds = next.mediaUnderstandingProviderIds;
|
||||
existing.imageGenerationProviderIds = next.imageGenerationProviderIds;
|
||||
existing.webSearchProviderIds = next.webSearchProviderIds;
|
||||
existing.toolNames = next.toolNames;
|
||||
}
|
||||
export const pluginRegistrationContractRegistry: PluginRegistrationContractEntry[] =
|
||||
loadedBundledCapabilityRegistry.plugins
|
||||
.filter(
|
||||
(plugin) =>
|
||||
plugin.origin === "bundled" &&
|
||||
(plugin.providerIds.length > 0 ||
|
||||
plugin.speechProviderIds.length > 0 ||
|
||||
plugin.mediaUnderstandingProviderIds.length > 0 ||
|
||||
plugin.imageGenerationProviderIds.length > 0 ||
|
||||
plugin.webSearchProviderIds.length > 0 ||
|
||||
plugin.toolNames.length > 0),
|
||||
)
|
||||
.map((plugin) => ({
|
||||
pluginId: plugin.id,
|
||||
providerIds: plugin.providerIds,
|
||||
speechProviderIds: plugin.speechProviderIds,
|
||||
mediaUnderstandingProviderIds: plugin.mediaUnderstandingProviderIds,
|
||||
imageGenerationProviderIds: plugin.imageGenerationProviderIds,
|
||||
webSearchProviderIds: plugin.webSearchProviderIds,
|
||||
toolNames: plugin.toolNames,
|
||||
}));
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user