Files
openclaw/src/plugins/provider-discovery.runtime.ts
Peter Steinberger 77d9ac30bb refactor: reuse shared coercion helpers (#86419)
* refactor: share talk event metric extraction

* refactor: reuse shared coercion helpers

* refactor: reuse shared primitive guards

* refactor: reuse shared record guard

* refactor: reuse shared primitive helpers

* refactor: reuse shared string guards

* refactor: reuse shared non-empty string guard

* refactor: share plugin primitive coercion helpers

* refactor: reuse plugin coercion helpers

* refactor: reuse plugin coercion helpers in more plugins

* refactor: reuse channel coercion helpers

* refactor: reuse monitor coercion helpers

* refactor: reuse provider coercion helpers

* refactor: reuse core coercion helpers

* refactor: reuse runtime coercion helpers

* refactor: reuse helper coercion in codex paths

* refactor: reuse helper coercion in runtime paths

* refactor: reuse codex app-server coercion helpers

* refactor: reuse codex record helpers

* refactor: reuse migration and qa record helpers

* refactor: reuse feishu and core helper guards

* refactor: reuse browser and policy coercion helpers

* refactor: reuse memory wiki record helper

* refactor: share boolean coercion helpers

* refactor: reuse finite number coercion

* refactor: reuse trimmed string list helpers

* refactor: reuse string list normalization

* refactor: reuse remaining string list helpers

* refactor: reuse string entry normalizer

* refactor: share sorted string helpers

* refactor: share string list normalization

* test: preserve command registry browser imports

* refactor: reuse trimmed list helpers

* refactor: reuse string dedupe helpers

* refactor: reuse local dedupe helpers

* refactor: reuse more string dedupe helpers

* refactor: reuse command string dedupe helpers

* refactor: dedupe memory path lists with helper

* refactor: expose string dedupe helpers to plugins

* refactor: reuse core string dedupe helpers

* refactor: reuse shared unique value helpers

* refactor: reuse unique helpers in agent utilities

* refactor: reuse unique helpers in config plumbing

* refactor: reuse unique helpers in extensions

* refactor: reuse unique helpers in core utilities

* refactor: reuse unique helpers in qa plugins

* refactor: reuse unique helpers in memory plugins

* refactor: reuse unique helpers in channel plugins

* refactor: reuse unique helpers in core tails

* refactor: reuse unique helper in comfy workflow

* refactor: reuse unique helpers in test utilities

* refactor: expose unique value helper to plugins

* refactor: reuse unique helpers for numeric lists

* refactor: replace index dedupe filters

* refactor: reuse string entry normalization

* refactor: reuse string normalization in plugin helpers

* refactor: reuse string normalization in extension helpers

* refactor: reuse string normalization in channel parsers

* refactor: reuse string normalization in memory search

* refactor: reuse string normalization in provider parsers

* refactor: reuse string normalization in qa helpers

* refactor: reuse string normalization in infra parsers

* refactor: reuse string normalization in messaging parsers

* refactor: reuse string normalization in core parsers

* refactor: reuse string normalization in extension parsers

* refactor: reuse string normalization in remaining parsers

* refactor: reuse string normalization in final parser spots

* refactor: reuse string normalization in qa media helpers

* refactor: reuse normalization in provider and media lists

* refactor: reuse normalization for remaining set filters

* refactor: reuse normalization in policy allowlists

* refactor: reuse normalization in session and owner lists

* refactor: centralize primitive string lists

* refactor: reuse lowercase entry helpers

* refactor: reuse sorted string helpers

* refactor: reuse unique trimmed helpers

* refactor: reuse string normalization helpers

* refactor: reuse catalog string helpers

* refactor: reuse remaining string helpers

* refactor: simplify remaining list normalization

* refactor: reuse codex auth order normalization

* chore: refresh plugin sdk api baseline

* fix: make shared string sorting deterministic

* chore: refresh plugin sdk api baseline

* fix: align host env security ordering
2026-05-25 21:20:41 +01:00

182 lines
6.6 KiB
TypeScript

import type { OpenClawConfig } from "../config/types.openclaw.js";
import { sortUniqueStrings } from "../shared/string-normalization.js";
import { loadManifestMetadataSnapshot } from "./manifest-contract-eligibility.js";
import type { PluginManifestRecord } from "./manifest-registry.js";
import type { PluginMetadataRegistryView } from "./plugin-metadata-snapshot.types.js";
import { resolveDiscoveredProviderPluginIds } from "./providers.js";
import { resolvePluginProviders } from "./providers.runtime.js";
import { createPluginSourceLoader } from "./source-loader.js";
import type { ProviderPlugin } from "./types.js";
type ProviderDiscoveryModule =
| ProviderPlugin
| ProviderPlugin[]
| {
default?: ProviderPlugin | ProviderPlugin[];
providers?: ProviderPlugin[];
provider?: ProviderPlugin;
};
type ProviderDiscoveryEntryResult = {
providers: ProviderPlugin[];
complete: boolean;
pluginRecords: PluginManifestRecord[];
entryPluginIds: Set<string>;
};
function normalizeDiscoveryModule(value: ProviderDiscoveryModule): ProviderPlugin[] {
const resolved =
value && typeof value === "object" && "default" in value && value.default !== undefined
? value.default
: value;
if (Array.isArray(resolved)) {
return resolved;
}
if (resolved && typeof resolved === "object" && "id" in resolved) {
return [resolved];
}
if (value && typeof value === "object" && !Array.isArray(value)) {
const record = value as { providers?: ProviderPlugin[]; provider?: ProviderPlugin };
if (Array.isArray(record.providers)) {
return record.providers;
}
if (record.provider) {
return [record.provider];
}
}
return [];
}
function hasLiveProviderDiscoveryHook(provider: ProviderPlugin): boolean {
return (
typeof provider.catalog?.run === "function" || typeof provider.discovery?.run === "function"
);
}
function hasProviderAuthEnvCredential(
plugin: PluginManifestRecord,
env: NodeJS.ProcessEnv,
): boolean {
const envVars = [
...(plugin.setup?.providers ?? []).flatMap((provider) => provider.envVars ?? []),
...Object.values(plugin.providerAuthEnvVars ?? {}).flat(),
];
return envVars.some((name) => {
const value = env[name]?.trim();
return value !== undefined && value !== "";
});
}
function resolveProviderDiscoveryEntryPlugins(params: {
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
onlyPluginIds?: string[];
includeUntrustedWorkspacePlugins?: boolean;
requireCompleteDiscoveryEntryCoverage?: boolean;
discoveryEntriesOnly?: boolean;
pluginMetadataSnapshot?: PluginMetadataRegistryView;
}): ProviderDiscoveryEntryResult {
const metadataSnapshot =
params.pluginMetadataSnapshot ??
loadManifestMetadataSnapshot({
config: params.config ?? {},
env: params.env ?? process.env,
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
});
const registry = metadataSnapshot.index;
const manifestRegistry = metadataSnapshot.manifestRegistry;
const pluginIds = resolveDiscoveredProviderPluginIds({
...params,
registry,
manifestRegistry,
});
const pluginIdSet = new Set(pluginIds);
const pluginRecords = manifestRegistry.plugins.filter((plugin) => pluginIdSet.has(plugin.id));
const entryRecords = pluginRecords.filter((plugin) => plugin.providerDiscoverySource);
const entryPluginIds = new Set(entryRecords.map((plugin) => plugin.id));
if (entryRecords.length === 0) {
return { providers: [], complete: false, pluginRecords, entryPluginIds };
}
const complete = entryRecords.length === pluginIdSet.size;
if (params.requireCompleteDiscoveryEntryCoverage && !complete) {
return { providers: [], complete: false, pluginRecords, entryPluginIds };
}
const loadSource = createPluginSourceLoader();
const providers: ProviderPlugin[] = [];
for (const manifest of entryRecords) {
try {
const moduleExport = loadSource(manifest.providerDiscoverySource!) as ProviderDiscoveryModule;
providers.push(
...normalizeDiscoveryModule(moduleExport).map((provider) =>
Object.assign({}, provider, { pluginId: manifest.id }),
),
);
} catch {
// Discovery fast path is optional. Fall back to the full plugin loader
// below so existing plugin diagnostics/load behavior remains canonical.
return { providers: [], complete: false, pluginRecords, entryPluginIds };
}
}
return { providers, complete, pluginRecords, entryPluginIds };
}
function resolveSelectiveFullPluginIds(params: {
entryResult: ProviderDiscoveryEntryResult;
entryProviders: ProviderPlugin[];
env: NodeJS.ProcessEnv;
}): string[] {
const staticOnlyEntryPluginIds = params.entryProviders
.filter((provider) => !hasLiveProviderDiscoveryHook(provider))
.map((provider) => provider.pluginId)
.filter((pluginId): pluginId is string => typeof pluginId === "string" && pluginId !== "");
const missingEntryCredentialPluginIds = params.entryResult.pluginRecords
.filter((plugin) => !params.entryResult.entryPluginIds.has(plugin.id))
.filter((plugin) => hasProviderAuthEnvCredential(plugin, params.env))
.map((plugin) => plugin.id);
return sortUniqueStrings([...staticOnlyEntryPluginIds, ...missingEntryCredentialPluginIds]);
}
export function resolvePluginDiscoveryProvidersRuntime(params: {
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
onlyPluginIds?: string[];
includeUntrustedWorkspacePlugins?: boolean;
requireCompleteDiscoveryEntryCoverage?: boolean;
discoveryEntriesOnly?: boolean;
pluginMetadataSnapshot?: PluginMetadataRegistryView;
}): ProviderPlugin[] {
const env = params.env ?? process.env;
const entryResult = resolveProviderDiscoveryEntryPlugins({ ...params, env });
if (params.discoveryEntriesOnly === true) {
return entryResult.providers;
}
const liveEntryProviders = entryResult.providers.filter(hasLiveProviderDiscoveryHook);
if (entryResult.complete && liveEntryProviders.length === entryResult.providers.length) {
return liveEntryProviders;
}
if (params.onlyPluginIds === undefined && entryResult.providers.length > 0) {
const fullPluginIds = resolveSelectiveFullPluginIds({
entryResult,
entryProviders: entryResult.providers,
env,
});
const fullProviders =
fullPluginIds.length > 0
? resolvePluginProviders({
...params,
env,
onlyPluginIds: fullPluginIds,
bundledProviderAllowlistCompat: true,
})
: [];
return [...liveEntryProviders, ...fullProviders];
}
return resolvePluginProviders({
...params,
env,
bundledProviderAllowlistCompat: true,
});
}