mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-12 09:36:07 +00:00
* feat: add utility model session titles * docs: clarify utility model calls * fix: budget utility title reasoning * docs: refresh config baseline * docs: refresh plugin sdk baseline * docs: refresh plugin sdk baseline --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
2331 lines
77 KiB
TypeScript
2331 lines
77 KiB
TypeScript
/** Resolves plugin ids that should load during Gateway startup. */
|
|
import { collectConfiguredModelRefs } from "@openclaw/model-catalog-core/configured-model-refs";
|
|
import {
|
|
buildModelCatalogMergeKey,
|
|
parseModelCatalogRef,
|
|
} from "@openclaw/model-catalog-core/model-catalog-refs";
|
|
import {
|
|
findNormalizedProviderValue,
|
|
normalizeProviderId,
|
|
} from "@openclaw/model-catalog-core/provider-id";
|
|
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
|
import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce";
|
|
import { collectConfiguredAgentHarnessRuntimes } from "../agents/harness-runtimes.js";
|
|
import { splitTrailingAuthProfile } from "../agents/model-ref-profile.js";
|
|
import {
|
|
listExplicitlyDisabledChannelIdsForConfig,
|
|
listPotentialConfiguredChannelIds,
|
|
} from "../channels/config-presence.js";
|
|
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
|
import {
|
|
DEFAULT_MEMORY_DREAMING_PLUGIN_ID,
|
|
resolveMemoryDreamingConfig,
|
|
resolveMemoryDreamingPluginConfig,
|
|
resolveMemoryDreamingPluginId,
|
|
} from "../memory-host-sdk/dreaming.js";
|
|
import { planManifestModelCatalogRows } from "../model-catalog/manifest-planner.js";
|
|
import { hasExplicitChannelConfig } from "./channel-presence-policy.js";
|
|
import { collectPluginConfigContractMatches } from "./config-contracts.js";
|
|
import { normalizePluginsConfigWithResolver } from "./config-normalization-shared.js";
|
|
import { resolveEffectivePluginActivationState } from "./config-state.js";
|
|
import { isPluginEnabledByDefaultForPlatform } from "./default-enablement.js";
|
|
import { resolveConfiguredGenericEmbeddingProviderId } from "./embedding-provider-config.js";
|
|
import { listRegisteredEmbeddingProviders } from "./embedding-providers.js";
|
|
import {
|
|
collectConfiguredSpeechProviderIds,
|
|
normalizeConfiguredSpeechProviderIdForStartup,
|
|
} from "./gateway-startup-speech-providers.js";
|
|
import { hashJson } from "./installed-plugin-index-hash.js";
|
|
import {
|
|
createInstalledPluginIndexScopeLookup,
|
|
type InstalledPluginIndexScopeLookup,
|
|
} from "./installed-plugin-index-scope-lookup.js";
|
|
import type { InstalledPluginIndex, InstalledPluginIndexRecord } from "./installed-plugin-index.js";
|
|
import type { PluginManifestRecord, PluginManifestRegistry } from "./manifest-registry.js";
|
|
import {
|
|
isPluginMetadataSnapshotCompatible,
|
|
resolvePluginMetadataSnapshot,
|
|
type PluginMetadataSnapshot,
|
|
} from "./plugin-metadata-snapshot.js";
|
|
import type { PluginMetadataSnapshotPluginIdScope } from "./plugin-metadata-snapshot.types.js";
|
|
import {
|
|
createPluginRegistryIdNormalizer,
|
|
normalizePluginsConfigWithRegistry,
|
|
} from "./plugin-registry-contributions.js";
|
|
import type { PluginRegistrySnapshot } from "./plugin-registry-snapshot.js";
|
|
import { normalizePluginIdScope } from "./plugin-scope.js";
|
|
import type { PluginRegistry } from "./registry-types.js";
|
|
|
|
export type GatewayStartupPluginPlan = {
|
|
channelPluginIds: readonly string[];
|
|
configuredDeferredChannelPluginIds: readonly string[];
|
|
pluginIds: readonly string[];
|
|
};
|
|
|
|
type NormalizedPluginsConfig = ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
|
type GenerationProviderContractKey =
|
|
| "imageGenerationProviders"
|
|
| "videoGenerationProviders"
|
|
| "musicGenerationProviders";
|
|
type VoiceProviderContractKey =
|
|
| "speechProviders"
|
|
| "realtimeTranscriptionProviders"
|
|
| "realtimeVoiceProviders";
|
|
type ConfiguredGenerationProviderIds = Record<GenerationProviderContractKey, ReadonlySet<string>>;
|
|
type ConfiguredVoiceProviderIds = Record<VoiceProviderContractKey, ReadonlySet<string>>;
|
|
const CORE_BUILT_IN_MODEL_APIS = new Set([
|
|
"anthropic-messages",
|
|
"azure-openai-responses",
|
|
"google-generative-ai",
|
|
"google-vertex",
|
|
"mistral-conversations",
|
|
"openai-chatgpt-responses",
|
|
"openai-completions",
|
|
"openai-responses",
|
|
]);
|
|
|
|
function sortUniquePluginIds(values: Iterable<string>): string[] {
|
|
return [...new Set([...values].map((value) => value.trim()).filter(Boolean))].toSorted(
|
|
(left, right) => left.localeCompare(right),
|
|
);
|
|
}
|
|
|
|
function normalizePluginsConfigForInstalledIndex(
|
|
config: OpenClawConfig["plugins"] | undefined,
|
|
lookup: InstalledPluginIndexScopeLookup,
|
|
) {
|
|
return normalizePluginsConfigWithResolver(config, lookup.normalizePluginId);
|
|
}
|
|
|
|
function isConfigActivationValueEnabled(value: unknown): boolean {
|
|
if (value === false) {
|
|
return false;
|
|
}
|
|
if (isRecord(value) && value.enabled === false) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function listPotentialEnabledChannelIds(config: OpenClawConfig, env: NodeJS.ProcessEnv): string[] {
|
|
const disabled = new Set(listExplicitlyDisabledChannelIdsForConfig(config));
|
|
return listPotentialConfiguredChannelIds(config, env, { includePersistedAuthState: false })
|
|
.map((id) => normalizeOptionalLowercaseString(id) ?? "")
|
|
.filter((id) => id && !disabled.has(id));
|
|
}
|
|
|
|
function isGatewayStartupMemoryPlugin(plugin: InstalledPluginIndexRecord): boolean {
|
|
return plugin.startup.memory;
|
|
}
|
|
|
|
function resolveGatewayStartupDreamingEngineId(config: OpenClawConfig): string | undefined {
|
|
const dreamingConfig = resolveMemoryDreamingConfig({
|
|
pluginConfig: resolveMemoryDreamingPluginConfig(config),
|
|
cfg: config,
|
|
});
|
|
if (!dreamingConfig.enabled) {
|
|
return undefined;
|
|
}
|
|
if (!resolveGatewayStartupDreamingSelectedPluginId(config)) {
|
|
return undefined;
|
|
}
|
|
return DEFAULT_MEMORY_DREAMING_PLUGIN_ID;
|
|
}
|
|
|
|
function resolveGatewayStartupDreamingSelectedPluginId(config: OpenClawConfig): string | undefined {
|
|
const selectedPluginId = normalizeOptionalLowercaseString(resolveMemoryDreamingPluginId(config));
|
|
return selectedPluginId && selectedPluginId !== DEFAULT_MEMORY_DREAMING_PLUGIN_ID
|
|
? selectedPluginId
|
|
: undefined;
|
|
}
|
|
|
|
function blocksPluginStartup(params: {
|
|
pluginId: string;
|
|
pluginsConfig: NormalizedPluginsConfig;
|
|
activationSourcePlugins: NormalizedPluginsConfig;
|
|
}): boolean {
|
|
return (
|
|
params.pluginsConfig.deny.includes(params.pluginId) ||
|
|
params.activationSourcePlugins.deny.includes(params.pluginId) ||
|
|
params.pluginsConfig.entries[params.pluginId]?.enabled === false ||
|
|
params.activationSourcePlugins.entries[params.pluginId]?.enabled === false
|
|
);
|
|
}
|
|
|
|
function resolveAuthorizedGatewayStartupDreamingPluginIds(params: {
|
|
config: OpenClawConfig;
|
|
pluginsConfig: NormalizedPluginsConfig;
|
|
activationSource: {
|
|
plugins: NormalizedPluginsConfig;
|
|
rootConfig?: OpenClawConfig;
|
|
};
|
|
activationSourcePlugins: NormalizedPluginsConfig;
|
|
selectedMemoryPluginId?: string;
|
|
index: { plugins: readonly InstalledPluginIndexRecord[] };
|
|
platform?: NodeJS.Platform;
|
|
}): Set<string> {
|
|
const engineId = resolveGatewayStartupDreamingEngineId(params.config);
|
|
const dreamingSelectedPluginId = resolveGatewayStartupDreamingSelectedPluginId(params.config);
|
|
if (!engineId || !params.pluginsConfig.enabled || !params.activationSourcePlugins.enabled) {
|
|
return new Set();
|
|
}
|
|
if (
|
|
!params.selectedMemoryPluginId ||
|
|
params.selectedMemoryPluginId !== dreamingSelectedPluginId ||
|
|
params.selectedMemoryPluginId === engineId ||
|
|
blocksPluginStartup({
|
|
pluginId: engineId,
|
|
pluginsConfig: params.pluginsConfig,
|
|
activationSourcePlugins: params.activationSourcePlugins,
|
|
})
|
|
) {
|
|
return new Set();
|
|
}
|
|
const selectedPlugin = params.index.plugins.find(
|
|
(plugin) => plugin.pluginId === params.selectedMemoryPluginId,
|
|
);
|
|
const sidecarPlugin = params.index.plugins.find((plugin) => plugin.pluginId === engineId);
|
|
if (!selectedPlugin?.startup.memory || !sidecarPlugin?.startup.memory) {
|
|
return new Set();
|
|
}
|
|
const activationState = resolveEffectivePluginActivationState({
|
|
id: selectedPlugin.pluginId,
|
|
origin: selectedPlugin.origin,
|
|
config: params.pluginsConfig,
|
|
rootConfig: params.config,
|
|
enabledByDefault: isPluginEnabledByDefaultForPlatform(selectedPlugin, params.platform),
|
|
activationSource: params.activationSource,
|
|
});
|
|
return activationState.enabled ? new Set([engineId]) : new Set();
|
|
}
|
|
|
|
function resolveMemorySlotStartupPluginId(params: {
|
|
activationSourceConfig: OpenClawConfig;
|
|
activationSourcePlugins: ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
|
normalizePluginId: (pluginId: string) => string;
|
|
}): string | undefined {
|
|
const { activationSourceConfig, activationSourcePlugins, normalizePluginId } = params;
|
|
const configuredSlot = activationSourceConfig.plugins?.slots?.memory?.trim();
|
|
if (configuredSlot?.toLowerCase() === "none") {
|
|
return undefined;
|
|
}
|
|
if (!configuredSlot) {
|
|
const defaultSlot = activationSourcePlugins.slots.memory;
|
|
if (typeof defaultSlot !== "string") {
|
|
return undefined;
|
|
}
|
|
if (
|
|
activationSourcePlugins.allow.length > 0 &&
|
|
!activationSourcePlugins.allow.includes(defaultSlot)
|
|
) {
|
|
return undefined;
|
|
}
|
|
return defaultSlot;
|
|
}
|
|
return normalizePluginId(configuredSlot);
|
|
}
|
|
|
|
function resolveContextEngineSlotStartupPluginId(params: {
|
|
activationSourceConfig: OpenClawConfig;
|
|
activationSourcePlugins: ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
|
normalizePluginId: (pluginId: string) => string;
|
|
}): string | undefined {
|
|
const { activationSourceConfig, activationSourcePlugins, normalizePluginId } = params;
|
|
const configuredSlot = activationSourceConfig.plugins?.slots?.contextEngine?.trim();
|
|
if (!configuredSlot) {
|
|
return undefined;
|
|
}
|
|
const normalized = normalizePluginId(configuredSlot);
|
|
// "legacy" is the built-in default engine — no plugin startup needed.
|
|
if (normalized === "legacy") {
|
|
return undefined;
|
|
}
|
|
if (activationSourcePlugins.deny.includes(normalized)) {
|
|
return undefined;
|
|
}
|
|
if (activationSourcePlugins.entries[normalized]?.enabled === false) {
|
|
return undefined;
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
function shouldConsiderForGatewayStartup(params: {
|
|
plugin: InstalledPluginIndexRecord;
|
|
manifest: PluginManifestRecord | undefined;
|
|
startupDreamingPluginIds: ReadonlySet<string>;
|
|
memorySlotStartupPluginId?: string;
|
|
contextEngineSlotStartupPluginId?: string;
|
|
}): boolean {
|
|
if (params.manifest?.activation?.onStartup === true) {
|
|
return true;
|
|
}
|
|
if (params.contextEngineSlotStartupPluginId === params.plugin.pluginId) {
|
|
return true;
|
|
}
|
|
if (!isGatewayStartupMemoryPlugin(params.plugin)) {
|
|
return false;
|
|
}
|
|
if (params.startupDreamingPluginIds.has(params.plugin.pluginId)) {
|
|
return true;
|
|
}
|
|
return params.memorySlotStartupPluginId === params.plugin.pluginId;
|
|
}
|
|
|
|
function hasConfiguredStartupChannel(params: {
|
|
plugin: InstalledPluginIndexRecord;
|
|
manifestLookup: ManifestRegistryLookup;
|
|
configuredChannelIds: ReadonlySet<string>;
|
|
}): boolean {
|
|
return listManifestChannelIds(params.manifestLookup, params.plugin.pluginId).some((channelId) =>
|
|
params.configuredChannelIds.has(channelId),
|
|
);
|
|
}
|
|
|
|
type ManifestRegistryLookup = ReadonlyMap<string, PluginManifestRecord>;
|
|
|
|
function createManifestRegistryLookup(
|
|
manifestRegistry: PluginManifestRegistry,
|
|
): ManifestRegistryLookup {
|
|
return new Map(manifestRegistry.plugins.map((plugin) => [plugin.id, plugin]));
|
|
}
|
|
|
|
function listManifestChannelIds(
|
|
manifestLookup: ManifestRegistryLookup,
|
|
pluginId: string,
|
|
): readonly string[] {
|
|
return manifestLookup.get(pluginId)?.channels ?? [];
|
|
}
|
|
|
|
function findManifestPlugin(
|
|
manifestLookup: ManifestRegistryLookup,
|
|
pluginId: string,
|
|
): PluginManifestRecord | undefined {
|
|
return manifestLookup.get(pluginId);
|
|
}
|
|
|
|
function hasConfiguredActivationPath(params: {
|
|
manifest: PluginManifestRecord | undefined;
|
|
config: OpenClawConfig;
|
|
}): boolean {
|
|
return hasConfiguredActivationPathPatterns({
|
|
paths: params.manifest?.activation?.onConfigPaths,
|
|
config: params.config,
|
|
});
|
|
}
|
|
|
|
function hasConfiguredActivationPathPatterns(params: {
|
|
paths: readonly string[] | undefined;
|
|
config: OpenClawConfig;
|
|
}): boolean {
|
|
const paths = params.paths;
|
|
if (!paths?.length) {
|
|
return false;
|
|
}
|
|
return paths.some((pathPattern) =>
|
|
collectPluginConfigContractMatches({
|
|
root: params.config,
|
|
pathPattern,
|
|
}).some((match) => isConfigActivationValueEnabled(match.value)),
|
|
);
|
|
}
|
|
|
|
function addConfiguredActivationPathPluginIds(
|
|
target: Set<string>,
|
|
params: {
|
|
activationSourceConfig: OpenClawConfig;
|
|
index: InstalledPluginIndex;
|
|
},
|
|
): void {
|
|
for (const plugin of params.index.plugins) {
|
|
if (plugin.origin !== "bundled") {
|
|
continue;
|
|
}
|
|
if (
|
|
hasConfiguredActivationPathPatterns({
|
|
paths: plugin.startup.configPaths,
|
|
config: params.activationSourceConfig,
|
|
})
|
|
) {
|
|
target.add(plugin.pluginId);
|
|
}
|
|
}
|
|
}
|
|
|
|
function manifestOwnsConfiguredSpeechProvider(params: {
|
|
manifest: PluginManifestRecord | undefined;
|
|
configuredSpeechProviderIds: ReadonlySet<string>;
|
|
}): boolean {
|
|
if (params.configuredSpeechProviderIds.size === 0) {
|
|
return false;
|
|
}
|
|
return (params.manifest?.contracts?.speechProviders ?? []).some((providerId) => {
|
|
const normalized = normalizeConfiguredSpeechProviderIdForStartup(providerId);
|
|
return normalized ? params.configuredSpeechProviderIds.has(normalized) : false;
|
|
});
|
|
}
|
|
|
|
function collectConfiguredWebSearchProviderIds(config: OpenClawConfig): ReadonlySet<string> {
|
|
const search = config.tools?.web?.search;
|
|
if (search?.enabled === false || typeof search?.provider !== "string") {
|
|
return new Set();
|
|
}
|
|
const providerId = normalizeOptionalLowercaseString(search.provider);
|
|
return providerId ? new Set([providerId]) : new Set();
|
|
}
|
|
|
|
function manifestOwnsConfiguredWebSearchProvider(params: {
|
|
manifest: PluginManifestRecord | undefined;
|
|
configuredWebSearchProviderIds: ReadonlySet<string>;
|
|
}): boolean {
|
|
if (params.configuredWebSearchProviderIds.size === 0) {
|
|
return false;
|
|
}
|
|
return (params.manifest?.contracts?.webSearchProviders ?? []).some((providerId) => {
|
|
const normalized = normalizeOptionalLowercaseString(providerId);
|
|
return normalized ? params.configuredWebSearchProviderIds.has(normalized) : false;
|
|
});
|
|
}
|
|
|
|
function listModelProviderRefs(value: unknown): string[] {
|
|
if (typeof value === "string") {
|
|
return [value];
|
|
}
|
|
if (!isRecord(value)) {
|
|
return [];
|
|
}
|
|
const refs: string[] = [];
|
|
if (typeof value.primary === "string") {
|
|
refs.push(value.primary);
|
|
}
|
|
if (Array.isArray(value.fallbacks)) {
|
|
for (const fallback of value.fallbacks) {
|
|
if (typeof fallback === "string") {
|
|
refs.push(fallback);
|
|
}
|
|
}
|
|
}
|
|
return refs;
|
|
}
|
|
|
|
function listModelProviderRefParts(value: unknown): Array<{ providerId: string; modelId: string }> {
|
|
return listModelProviderRefs(value)
|
|
.map(parseModelCatalogRef)
|
|
.filter((entry): entry is NonNullable<typeof entry> => entry !== null)
|
|
.map(({ provider, modelId }) => ({ providerId: provider, modelId }));
|
|
}
|
|
|
|
function collectModelProviderIds(value: unknown): ReadonlySet<string> {
|
|
return new Set(
|
|
listModelProviderRefs(value)
|
|
.map((ref) => {
|
|
const slashIndex = ref.indexOf("/");
|
|
return slashIndex > 0 ? normalizeProviderId(ref.slice(0, slashIndex)) : "";
|
|
})
|
|
.filter((providerId): providerId is string => Boolean(providerId)),
|
|
);
|
|
}
|
|
|
|
type ManifestModelProviderLookup = {
|
|
modelApis: ReadonlyMap<string, string>;
|
|
providerIds: ReadonlySet<string>;
|
|
};
|
|
|
|
function buildManifestModelProviderLookup(
|
|
manifestRegistry: PluginManifestRegistry,
|
|
): ManifestModelProviderLookup {
|
|
const modelApis = new Map(
|
|
planManifestModelCatalogRows({ registry: manifestRegistry }).rows.flatMap((row) =>
|
|
row.api ? [[row.mergeKey, row.api] as const] : [],
|
|
),
|
|
);
|
|
return {
|
|
modelApis,
|
|
providerIds: new Set(
|
|
manifestRegistry.plugins.flatMap((plugin) => plugin.providers.map(normalizeProviderId)),
|
|
),
|
|
};
|
|
}
|
|
|
|
function collectConfiguredAgentModelProviderIds(
|
|
config: OpenClawConfig,
|
|
manifestRegistry: PluginManifestRegistry,
|
|
): ReadonlySet<string> {
|
|
const modelIdsByProvider = new Map<string, Set<string>>();
|
|
const manifestModelProviders = buildManifestModelProviderLookup(manifestRegistry);
|
|
const addModelProviderRefs = (value: unknown) => {
|
|
for (const { providerId, modelId } of listModelProviderRefParts(value)) {
|
|
const modelIds = modelIdsByProvider.get(providerId) ?? new Set<string>();
|
|
modelIds.add(modelId);
|
|
modelIdsByProvider.set(providerId, modelIds);
|
|
}
|
|
};
|
|
const addModelMapProviderIds = (models: unknown) => {
|
|
if (!isRecord(models)) {
|
|
return;
|
|
}
|
|
for (const modelRef of Object.keys(models)) {
|
|
addModelProviderRefs(modelRef);
|
|
}
|
|
};
|
|
|
|
const defaults = config.agents?.defaults;
|
|
addModelProviderRefs(defaults?.model);
|
|
addModelProviderRefs(defaults?.utilityModel);
|
|
addModelMapProviderIds(defaults?.models);
|
|
|
|
const agents = Array.isArray(config.agents?.list) ? config.agents.list : [];
|
|
for (const agent of agents) {
|
|
if (!isRecord(agent)) {
|
|
continue;
|
|
}
|
|
addModelProviderRefs(agent.model);
|
|
addModelProviderRefs(agent.utilityModel);
|
|
addModelMapProviderIds(agent.models);
|
|
}
|
|
|
|
return new Set(
|
|
[...modelIdsByProvider.entries()]
|
|
.filter(([providerId, modelIds]) => {
|
|
return [...modelIds].some((modelId) =>
|
|
configuredModelProviderNeedsRuntimePlugin({
|
|
config,
|
|
manifestModelProviders,
|
|
providerId,
|
|
modelId,
|
|
}),
|
|
);
|
|
})
|
|
.map(([providerId]) => providerId),
|
|
);
|
|
}
|
|
|
|
function configuredModelProviderNeedsRuntimePlugin(params: {
|
|
config: OpenClawConfig;
|
|
manifestModelProviders: ManifestModelProviderLookup;
|
|
providerId: string;
|
|
modelId: string;
|
|
}): boolean {
|
|
const providerConfig = params.config.models?.providers?.[params.providerId];
|
|
const configuredModel = providerConfig?.models?.find((model) => model.id === params.modelId);
|
|
const modelApi =
|
|
configuredModel?.api ??
|
|
providerConfig?.api ??
|
|
params.manifestModelProviders.modelApis.get(
|
|
buildModelCatalogMergeKey(params.providerId, params.modelId),
|
|
);
|
|
if (typeof modelApi === "string") {
|
|
return !CORE_BUILT_IN_MODEL_APIS.has(modelApi);
|
|
}
|
|
return params.manifestModelProviders.providerIds.has(params.providerId);
|
|
}
|
|
|
|
function manifestOwnsConfiguredModelProvider(params: {
|
|
manifest: PluginManifestRecord | undefined;
|
|
configuredModelProviderIds: ReadonlySet<string>;
|
|
}): boolean {
|
|
if (params.configuredModelProviderIds.size === 0) {
|
|
return false;
|
|
}
|
|
return (params.manifest?.providers ?? []).some((providerId) => {
|
|
return params.configuredModelProviderIds.has(normalizeProviderId(providerId));
|
|
});
|
|
}
|
|
|
|
function collectConfiguredGenerationProviderIds(
|
|
config: OpenClawConfig,
|
|
): ConfiguredGenerationProviderIds {
|
|
const defaults = config.agents?.defaults;
|
|
return {
|
|
imageGenerationProviders: collectModelProviderIds(defaults?.imageGenerationModel),
|
|
videoGenerationProviders: collectModelProviderIds(defaults?.videoGenerationModel),
|
|
musicGenerationProviders: collectModelProviderIds(defaults?.musicGenerationModel),
|
|
};
|
|
}
|
|
|
|
function collectConfiguredVoiceProviderIds(config: OpenClawConfig): ConfiguredVoiceProviderIds {
|
|
const providerIds = collectModelProviderIds(config.agents?.defaults?.voiceModel);
|
|
return {
|
|
speechProviders: providerIds,
|
|
realtimeTranscriptionProviders: providerIds,
|
|
realtimeVoiceProviders: providerIds,
|
|
};
|
|
}
|
|
|
|
// Explicit memory provider startup pulls plugin-owned providers into Gateway
|
|
// boot. Missing/"auto" stays lazy, and "none" disables provider-backed embeddings.
|
|
const MEMORY_EMBEDDING_PROVIDER_STARTUP_SKIP_IDS: ReadonlySet<string> = new Set(["auto", "none"]);
|
|
|
|
function normalizeMemoryEmbeddingProviderIdValue(value: unknown): string | undefined {
|
|
if (typeof value !== "string") {
|
|
return undefined;
|
|
}
|
|
const normalized = normalizeOptionalLowercaseString(value);
|
|
return normalized || undefined;
|
|
}
|
|
|
|
function normalizeExplicitMemoryEmbeddingProviderId(value: unknown): string | undefined {
|
|
const normalized = normalizeMemoryEmbeddingProviderIdValue(value);
|
|
return normalized && !MEMORY_EMBEDDING_PROVIDER_STARTUP_SKIP_IDS.has(normalized)
|
|
? normalized
|
|
: undefined;
|
|
}
|
|
|
|
function readMemorySearchEnabled(
|
|
memorySearch: Record<string, unknown> | undefined,
|
|
): boolean | undefined {
|
|
const enabled = memorySearch?.enabled;
|
|
return typeof enabled === "boolean" ? enabled : undefined;
|
|
}
|
|
|
|
function isMemorySlotExplicitlyDisabled(config: OpenClawConfig): boolean {
|
|
return normalizeOptionalLowercaseString(config.plugins?.slots?.memory) === "none";
|
|
}
|
|
|
|
export type MemoryEmbeddingStartupProviderSource = "provider" | "fallback";
|
|
|
|
export type ConfiguredMemoryEmbeddingStartupProviderOwner = {
|
|
/** Raw memory-search provider id as configured (normalized). */
|
|
configuredId: string;
|
|
/**
|
|
* Adapter ids a plugin can own for this provider: the configured id plus its
|
|
* `models.providers.<id>.api` owner when a custom provider maps to one.
|
|
*/
|
|
ownerIds: ReadonlySet<string>;
|
|
source: MemoryEmbeddingStartupProviderSource;
|
|
};
|
|
|
|
/**
|
|
* Resolve a configured memory embedding provider id to the adapter id(s) a
|
|
* plugin manifest contract or runtime registry can own. Mirrors runtime
|
|
* `getConfiguredMemoryEmbeddingProvider`: the raw id maps to a direct adapter,
|
|
* and a custom `models.providers.<id>` entry additionally maps to its `api`
|
|
* owner adapter (`provider: "ollama-5080"` with `api: "ollama"` -> "ollama").
|
|
* Both candidates are returned so matching covers the direct adapter and the
|
|
* API owner without the runtime adapter registry.
|
|
*/
|
|
function resolveMemoryEmbeddingProviderOwnerIds(
|
|
providerId: string,
|
|
config: OpenClawConfig,
|
|
): string[] {
|
|
const ownerIds = [providerId];
|
|
const genericOwnerId = normalizeOptionalLowercaseString(
|
|
resolveConfiguredGenericEmbeddingProviderId(providerId, config),
|
|
);
|
|
if (genericOwnerId && genericOwnerId !== providerId) {
|
|
ownerIds.push(genericOwnerId);
|
|
}
|
|
const ownerApi = normalizeOptionalLowercaseString(
|
|
findNormalizedProviderValue(config.models?.providers, providerId)?.api,
|
|
);
|
|
if (ownerApi && ownerApi !== providerId) {
|
|
ownerIds.push(ownerApi);
|
|
}
|
|
return ownerIds;
|
|
}
|
|
|
|
function resolveEffectiveMemoryEmbeddingProviderEntries(
|
|
defaults: Record<string, unknown> | undefined,
|
|
override: Record<string, unknown> | undefined,
|
|
): Array<{
|
|
configuredId: string;
|
|
source: MemoryEmbeddingStartupProviderSource;
|
|
}> {
|
|
const enabled = readMemorySearchEnabled(override) ?? readMemorySearchEnabled(defaults) ?? true;
|
|
if (!enabled) {
|
|
return [];
|
|
}
|
|
const rawProvider = normalizeMemoryEmbeddingProviderIdValue(
|
|
override?.provider ?? defaults?.provider,
|
|
);
|
|
const effectiveProvider = rawProvider === "auto" || !rawProvider ? "openai" : rawProvider;
|
|
if (effectiveProvider === "none") {
|
|
return [];
|
|
}
|
|
const entries: Array<{
|
|
configuredId: string;
|
|
source: MemoryEmbeddingStartupProviderSource;
|
|
}> = [];
|
|
const provider =
|
|
rawProvider && !MEMORY_EMBEDDING_PROVIDER_STARTUP_SKIP_IDS.has(rawProvider)
|
|
? rawProvider
|
|
: undefined;
|
|
if (provider) {
|
|
entries.push({ configuredId: provider, source: "provider" });
|
|
}
|
|
const fallback = normalizeExplicitMemoryEmbeddingProviderId(
|
|
override?.fallback ?? defaults?.fallback ?? "none",
|
|
);
|
|
if (fallback && fallback !== effectiveProvider) {
|
|
entries.push({ configuredId: fallback, source: "fallback" });
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
/**
|
|
* Collect explicit memory embedding provider owners required by startup. The
|
|
* resolver mirrors runtime memory-search inheritance for enablement, primary
|
|
* provider, and fallback provider, then maps custom `models.providers` ids to
|
|
* their API-owner adapter ids.
|
|
*/
|
|
export function collectConfiguredMemoryEmbeddingStartupProviderOwners(
|
|
config: OpenClawConfig,
|
|
): ConfiguredMemoryEmbeddingStartupProviderOwner[] {
|
|
if (isMemorySlotExplicitlyDisabled(config)) {
|
|
return [];
|
|
}
|
|
const byConfiguredIdAndSource = new Map<string, ConfiguredMemoryEmbeddingStartupProviderOwner>();
|
|
const defaultsBlock = config.agents?.defaults?.memorySearch;
|
|
const defaults = isRecord(defaultsBlock) ? defaultsBlock : undefined;
|
|
const addEffectiveProviders = (override: Record<string, unknown> | undefined) => {
|
|
for (const { configuredId, source } of resolveEffectiveMemoryEmbeddingProviderEntries(
|
|
defaults,
|
|
override,
|
|
)) {
|
|
const key = `${source}\0${configuredId}`;
|
|
if (byConfiguredIdAndSource.has(key)) {
|
|
continue;
|
|
}
|
|
byConfiguredIdAndSource.set(key, {
|
|
configuredId,
|
|
ownerIds: new Set(resolveMemoryEmbeddingProviderOwnerIds(configuredId, config)),
|
|
source,
|
|
});
|
|
}
|
|
};
|
|
addEffectiveProviders(undefined);
|
|
const agents = config.agents?.list;
|
|
const agentEntries = Array.isArray(agents) ? agents.filter(isRecord) : [];
|
|
if (agentEntries.length === 0) {
|
|
return [...byConfiguredIdAndSource.values()];
|
|
}
|
|
for (const agent of agentEntries) {
|
|
addEffectiveProviders(isRecord(agent.memorySearch) ? agent.memorySearch : undefined);
|
|
}
|
|
return [...byConfiguredIdAndSource.values()];
|
|
}
|
|
|
|
/**
|
|
* Collect configured memory embedding provider ids that map to a plugin-owned
|
|
* memory embedding provider contract, including the resolved `api` owner for
|
|
* custom `models.providers` ids so the owning plugin loads at startup.
|
|
*/
|
|
export function collectConfiguredMemoryEmbeddingProviderIds(
|
|
config: OpenClawConfig,
|
|
): ReadonlySet<string> {
|
|
const providerIds = new Set<string>();
|
|
for (const provider of collectConfiguredMemoryEmbeddingStartupProviderOwners(config)) {
|
|
for (const ownerId of provider.ownerIds) {
|
|
providerIds.add(ownerId);
|
|
}
|
|
}
|
|
return providerIds;
|
|
}
|
|
|
|
/**
|
|
* Report configured memory embedding providers that no loaded plugin can serve.
|
|
* A provider is unregistered only when none of its resolved adapter ids (the
|
|
* configured id and its `models.providers.<id>.api` owner) was registered, so
|
|
* custom providers warn when their API-owner plugin is missing but stay quiet
|
|
* once that plugin loads.
|
|
*/
|
|
export function collectUnregisteredConfiguredMemoryEmbeddingProviders(params: {
|
|
config: OpenClawConfig;
|
|
registeredProviderIds: ReadonlySet<string>;
|
|
}): Array<{ configuredId: string; source: MemoryEmbeddingStartupProviderSource }> {
|
|
const configured = collectConfiguredMemoryEmbeddingStartupProviderOwners(params.config);
|
|
if (configured.length === 0) {
|
|
return [];
|
|
}
|
|
const registered = new Set(
|
|
[...params.registeredProviderIds]
|
|
.map((id) => normalizeOptionalLowercaseString(id))
|
|
.filter((id): id is string => Boolean(id)),
|
|
);
|
|
return configured
|
|
.filter((provider) => ![...provider.ownerIds].some((ownerId) => registered.has(ownerId)))
|
|
.map((provider) => ({ configuredId: provider.configuredId, source: provider.source }))
|
|
.toSorted(
|
|
(left, right) =>
|
|
left.configuredId.localeCompare(right.configuredId) ||
|
|
left.source.localeCompare(right.source),
|
|
);
|
|
}
|
|
|
|
// Registered embedding provider ids the loaded runtime can actually serve: the live
|
|
// registry's memory + general embedding providers plus the global/core embedding
|
|
// registry. Shared by gateway boot (the startup "configured but unregistered" warning)
|
|
// and the `/status plugins` drift line so both agree on what counts as "registered" and
|
|
// never diverge. The `{ provider: entry.adapter }` wrap makes the core registry entries
|
|
// match the registration shape so the id projection stays uniform across all three sources.
|
|
export function collectRegisteredEmbeddingProviderIds(
|
|
registry: Partial<Pick<PluginRegistry, "embeddingProviders" | "memoryEmbeddingProviders">>,
|
|
): Set<string> {
|
|
return new Set(
|
|
[
|
|
...(registry.memoryEmbeddingProviders ?? []),
|
|
...(registry.embeddingProviders ?? []),
|
|
...listRegisteredEmbeddingProviders().map((entry) => ({ provider: entry.adapter })),
|
|
].map((entry) => entry.provider.id),
|
|
);
|
|
}
|
|
|
|
function addPluginConfigEntryIds(
|
|
target: Set<string>,
|
|
plugins: ReturnType<typeof normalizePluginsConfigForInstalledIndex>,
|
|
): void {
|
|
for (const [pluginId, entry] of Object.entries(plugins.entries)) {
|
|
if (entry?.enabled !== false) {
|
|
target.add(pluginId);
|
|
}
|
|
}
|
|
}
|
|
|
|
function addConfiguredSlotPluginIds(
|
|
target: Set<string>,
|
|
params: {
|
|
activationSourceConfig: OpenClawConfig;
|
|
activationSourcePlugins: ReturnType<typeof normalizePluginsConfigForInstalledIndex>;
|
|
lookup: InstalledPluginIndexScopeLookup;
|
|
},
|
|
): void {
|
|
const memorySlot = resolveMemorySlotStartupPluginId({
|
|
activationSourceConfig: params.activationSourceConfig,
|
|
activationSourcePlugins: params.activationSourcePlugins,
|
|
normalizePluginId: params.lookup.normalizePluginId,
|
|
});
|
|
if (memorySlot) {
|
|
target.add(memorySlot);
|
|
}
|
|
const contextEngineSlot = resolveContextEngineSlotStartupPluginId({
|
|
activationSourceConfig: params.activationSourceConfig,
|
|
activationSourcePlugins: params.activationSourcePlugins,
|
|
normalizePluginId: params.lookup.normalizePluginId,
|
|
});
|
|
if (contextEngineSlot) {
|
|
target.add(contextEngineSlot);
|
|
}
|
|
}
|
|
|
|
function collectConfiguredStartupChannelIds(params: {
|
|
activationSourceConfig: OpenClawConfig;
|
|
config: OpenClawConfig;
|
|
env: NodeJS.ProcessEnv;
|
|
}): string[] {
|
|
return sortUniquePluginIds([
|
|
...listPotentialEnabledChannelIds(params.config, params.env),
|
|
...listPotentialEnabledChannelIds(params.activationSourceConfig, params.env),
|
|
]);
|
|
}
|
|
|
|
function collectValidationHeartbeatTargetChannelIds(config: OpenClawConfig): string[] {
|
|
const channelIds: string[] = [];
|
|
const pushTarget = (target: unknown) => {
|
|
if (typeof target !== "string") {
|
|
return;
|
|
}
|
|
const normalized = normalizeOptionalLowercaseString(target);
|
|
if (!normalized || normalized === "last" || normalized === "none") {
|
|
return;
|
|
}
|
|
channelIds.push(normalized);
|
|
};
|
|
pushTarget(config.agents?.defaults?.heartbeat?.target);
|
|
if (Array.isArray(config.agents?.list)) {
|
|
for (const agent of config.agents.list) {
|
|
pushTarget(agent?.heartbeat?.target);
|
|
}
|
|
}
|
|
return sortUniquePluginIds(channelIds);
|
|
}
|
|
|
|
function collectValidationChannelConfigIds(config: OpenClawConfig): string[] {
|
|
const channels = isRecord(config.channels) ? config.channels : null;
|
|
if (!channels) {
|
|
return [];
|
|
}
|
|
return Object.keys(channels)
|
|
.filter((channelId) => channelId !== "defaults" && channelId !== "modelByChannel")
|
|
.map((channelId) => normalizeOptionalLowercaseString(channelId) ?? "")
|
|
.filter(Boolean)
|
|
.toSorted((left, right) => left.localeCompare(right));
|
|
}
|
|
|
|
function collectConfigValidationChannelIds(params: {
|
|
config: OpenClawConfig;
|
|
env: NodeJS.ProcessEnv;
|
|
}): string[] {
|
|
return sortUniquePluginIds([
|
|
...collectValidationChannelConfigIds(params.config),
|
|
...collectConfiguredStartupChannelIds({
|
|
config: params.config,
|
|
activationSourceConfig: params.config,
|
|
env: params.env,
|
|
}),
|
|
...collectValidationHeartbeatTargetChannelIds(params.config),
|
|
]);
|
|
}
|
|
|
|
function collectConfiguredProviderIds(config: OpenClawConfig): string[] {
|
|
const configuredWebSearchProviderIds = collectConfiguredWebSearchProviderIds(config);
|
|
const configuredGenerationProviderIds = collectConfiguredGenerationProviderIds(config);
|
|
const configuredVoiceProviderIds = collectConfiguredVoiceProviderIds(config);
|
|
return sortUniquePluginIds([
|
|
...collectConfiguredSpeechProviderIds(config),
|
|
...configuredWebSearchProviderIds,
|
|
...configuredGenerationProviderIds.imageGenerationProviders,
|
|
...configuredGenerationProviderIds.videoGenerationProviders,
|
|
...configuredGenerationProviderIds.musicGenerationProviders,
|
|
...configuredVoiceProviderIds.speechProviders,
|
|
...configuredVoiceProviderIds.realtimeTranscriptionProviders,
|
|
...configuredVoiceProviderIds.realtimeVoiceProviders,
|
|
...collectConfiguredMemoryEmbeddingProviderIds(config),
|
|
]);
|
|
}
|
|
|
|
function collectValidationConfiguredProviderIds(config: OpenClawConfig): string[] {
|
|
const providerIds: string[] = [];
|
|
const pushProviderId = (value: unknown) => {
|
|
if (typeof value !== "string") {
|
|
return;
|
|
}
|
|
const normalized = normalizeOptionalLowercaseString(value);
|
|
if (normalized) {
|
|
providerIds.push(normalized);
|
|
}
|
|
};
|
|
const profiles = config.auth?.profiles;
|
|
if (profiles && typeof profiles === "object") {
|
|
for (const profile of Object.values(profiles)) {
|
|
if (isRecord(profile)) {
|
|
pushProviderId(profile.provider);
|
|
}
|
|
}
|
|
}
|
|
const providers = config.models?.providers;
|
|
if (providers && typeof providers === "object") {
|
|
for (const providerId of Object.keys(providers)) {
|
|
pushProviderId(providerId);
|
|
}
|
|
}
|
|
for (const ref of collectConfiguredModelRefs(config)) {
|
|
const slashIndex = ref.value.indexOf("/");
|
|
if (slashIndex > 0) {
|
|
pushProviderId(ref.value.slice(0, slashIndex));
|
|
}
|
|
}
|
|
pushProviderId(config.tools?.web?.search?.provider);
|
|
pushProviderId(config.tools?.web?.fetch?.provider);
|
|
return sortUniquePluginIds(providerIds);
|
|
}
|
|
|
|
function collectValidationConfiguredShorthandModelIds(config: OpenClawConfig): string[] {
|
|
return sortUniquePluginIds(
|
|
collectConfiguredModelRefs(config)
|
|
.map((ref) => ref.value)
|
|
.filter((ref) => !ref.includes("/"))
|
|
.map((ref) => splitTrailingAuthProfile(ref).model.trim())
|
|
.filter(Boolean),
|
|
);
|
|
}
|
|
|
|
function addRequiredAgentHarnessPluginIds(
|
|
target: Set<string>,
|
|
params: {
|
|
activationSourceConfig: OpenClawConfig;
|
|
config: OpenClawConfig;
|
|
index: InstalledPluginIndex;
|
|
pluginsConfig: ReturnType<typeof normalizePluginsConfigForInstalledIndex>;
|
|
activationSource: {
|
|
plugins: ReturnType<typeof normalizePluginsConfigForInstalledIndex>;
|
|
rootConfig?: OpenClawConfig;
|
|
};
|
|
env: NodeJS.ProcessEnv;
|
|
platform?: NodeJS.Platform;
|
|
},
|
|
): void {
|
|
const requiredAgentHarnessRuntimes = new Set(
|
|
collectConfiguredAgentHarnessRuntimes(params.activationSourceConfig, {
|
|
includeImplicitRuntimePreferences: false,
|
|
}),
|
|
);
|
|
if (requiredAgentHarnessRuntimes.size === 0) {
|
|
return;
|
|
}
|
|
for (const plugin of params.index.plugins) {
|
|
if (
|
|
canStartRequiredAgentHarnessPlugin({
|
|
plugin,
|
|
pluginsConfig: params.pluginsConfig,
|
|
activationSource: params.activationSource,
|
|
config: params.config,
|
|
requiredAgentHarnessRuntimes,
|
|
platform: params.platform,
|
|
})
|
|
) {
|
|
target.add(plugin.pluginId);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function resolveGatewayStartupMetadataPluginIds(params: {
|
|
config: OpenClawConfig;
|
|
activationSourceConfig?: OpenClawConfig;
|
|
env: NodeJS.ProcessEnv;
|
|
index: InstalledPluginIndex;
|
|
platform?: NodeJS.Platform;
|
|
}): string[] | undefined {
|
|
const lookup = createInstalledPluginIndexScopeLookup(params.index);
|
|
const activationSourceConfig = params.activationSourceConfig ?? params.config;
|
|
const pluginsConfig = normalizePluginsConfigForInstalledIndex(params.config.plugins, lookup);
|
|
const activationSourcePlugins = normalizePluginsConfigForInstalledIndex(
|
|
activationSourceConfig.plugins,
|
|
lookup,
|
|
);
|
|
if (!pluginsConfig.enabled || !activationSourcePlugins.enabled) {
|
|
return [];
|
|
}
|
|
if (
|
|
params.config.plugins?.bundledDiscovery === "compat" ||
|
|
activationSourceConfig.plugins?.bundledDiscovery === "compat"
|
|
) {
|
|
return undefined;
|
|
}
|
|
if (pluginsConfig.allow.length === 0 && activationSourcePlugins.allow.length === 0) {
|
|
return undefined;
|
|
}
|
|
|
|
const scope = new Set<string>([...pluginsConfig.allow, ...activationSourcePlugins.allow]);
|
|
addPluginConfigEntryIds(scope, pluginsConfig);
|
|
addPluginConfigEntryIds(scope, activationSourcePlugins);
|
|
|
|
const memorySlotStartupPluginId = resolveMemorySlotStartupPluginId({
|
|
activationSourceConfig,
|
|
activationSourcePlugins,
|
|
normalizePluginId: lookup.normalizePluginId,
|
|
});
|
|
addConfiguredSlotPluginIds(scope, {
|
|
activationSourceConfig,
|
|
activationSourcePlugins,
|
|
lookup,
|
|
});
|
|
for (const pluginId of resolveAuthorizedGatewayStartupDreamingPluginIds({
|
|
config: params.config,
|
|
pluginsConfig,
|
|
activationSource: {
|
|
plugins: activationSourcePlugins,
|
|
rootConfig: activationSourceConfig,
|
|
},
|
|
activationSourcePlugins,
|
|
selectedMemoryPluginId: memorySlotStartupPluginId,
|
|
index: params.index,
|
|
platform: params.platform,
|
|
})) {
|
|
scope.add(pluginId);
|
|
}
|
|
if (!lookup.hasCompleteConfigPathActivationMetadata()) {
|
|
return undefined;
|
|
}
|
|
addConfiguredActivationPathPluginIds(scope, {
|
|
activationSourceConfig,
|
|
index: params.index,
|
|
});
|
|
|
|
const configuredChannelIds = collectConfiguredStartupChannelIds({
|
|
config: params.config,
|
|
activationSourceConfig,
|
|
env: params.env,
|
|
});
|
|
if (!lookup.hasDirectChannelOwners(configuredChannelIds)) {
|
|
return undefined;
|
|
}
|
|
lookup.addDirectChannelOwners(scope, configuredChannelIds);
|
|
|
|
const configuredProviderIds = sortUniquePluginIds([
|
|
...collectConfiguredProviderIds(params.config),
|
|
...collectConfiguredProviderIds(activationSourceConfig),
|
|
...collectValidationConfiguredProviderIds(params.config),
|
|
...collectValidationConfiguredProviderIds(activationSourceConfig),
|
|
]);
|
|
if (!lookup.canResolveDirectProviderIds(configuredProviderIds, scope)) {
|
|
return undefined;
|
|
}
|
|
lookup.addDirectProviderOwners(scope, configuredProviderIds);
|
|
|
|
const configuredShorthandModelIds = sortUniquePluginIds([
|
|
...collectValidationConfiguredShorthandModelIds(params.config),
|
|
...collectValidationConfiguredShorthandModelIds(activationSourceConfig),
|
|
]);
|
|
if (!lookup.hasShorthandModelOwners(configuredShorthandModelIds)) {
|
|
return undefined;
|
|
}
|
|
lookup.addShorthandModelOwners(scope, configuredShorthandModelIds);
|
|
|
|
addRequiredAgentHarnessPluginIds(scope, {
|
|
activationSourceConfig,
|
|
config: params.config,
|
|
index: params.index,
|
|
pluginsConfig,
|
|
activationSource: {
|
|
plugins: activationSourcePlugins,
|
|
rootConfig: activationSourceConfig,
|
|
},
|
|
env: params.env,
|
|
platform: params.platform,
|
|
});
|
|
|
|
const deniedPluginIds = new Set([...pluginsConfig.deny, ...activationSourcePlugins.deny]);
|
|
for (const pluginId of deniedPluginIds) {
|
|
scope.delete(pluginId);
|
|
}
|
|
for (const [pluginId, entry] of Object.entries(pluginsConfig.entries)) {
|
|
if (entry?.enabled === false) {
|
|
scope.delete(pluginId);
|
|
}
|
|
}
|
|
for (const [pluginId, entry] of Object.entries(activationSourcePlugins.entries)) {
|
|
if (entry?.enabled === false) {
|
|
scope.delete(pluginId);
|
|
}
|
|
}
|
|
if (!lookup.hasInstalledPluginIds(scope)) {
|
|
return undefined;
|
|
}
|
|
return sortUniquePluginIds(scope);
|
|
}
|
|
|
|
export function createGatewayStartupMetadataPluginIdScope(params: {
|
|
config: OpenClawConfig;
|
|
activationSourceConfig?: OpenClawConfig;
|
|
env: NodeJS.ProcessEnv;
|
|
platform?: NodeJS.Platform;
|
|
}): PluginMetadataSnapshotPluginIdScope {
|
|
const configuredChannelIds = collectConfiguredStartupChannelIds({
|
|
config: params.config,
|
|
activationSourceConfig: params.activationSourceConfig ?? params.config,
|
|
env: params.env,
|
|
});
|
|
return {
|
|
key: hashJson({
|
|
kind: "gateway-startup",
|
|
config: params.config,
|
|
activationSourceConfig: params.activationSourceConfig ?? null,
|
|
configuredChannelIds,
|
|
platform: params.platform ?? null,
|
|
}),
|
|
resolve: ({ index }) =>
|
|
resolveGatewayStartupMetadataPluginIds({
|
|
config: params.config,
|
|
...(params.activationSourceConfig !== undefined
|
|
? { activationSourceConfig: params.activationSourceConfig }
|
|
: {}),
|
|
env: params.env,
|
|
index,
|
|
...(params.platform !== undefined ? { platform: params.platform } : {}),
|
|
}),
|
|
};
|
|
}
|
|
|
|
function addValidationPluginConfigReferences(
|
|
target: Set<string>,
|
|
params: {
|
|
config: OpenClawConfig;
|
|
pluginsConfig: ReturnType<typeof normalizePluginsConfigForInstalledIndex>;
|
|
normalizePluginId: (pluginId: string) => string;
|
|
},
|
|
): void {
|
|
for (const pluginId of params.pluginsConfig.allow) {
|
|
target.add(pluginId);
|
|
}
|
|
for (const pluginId of params.pluginsConfig.deny) {
|
|
target.add(pluginId);
|
|
}
|
|
for (const pluginId of Object.keys(params.pluginsConfig.entries)) {
|
|
target.add(pluginId);
|
|
}
|
|
const rawSlots = isRecord(params.config.plugins?.slots) ? params.config.plugins.slots : {};
|
|
const hasExplicitMemorySlot = Object.hasOwn(rawSlots, "memory");
|
|
const memorySlot = hasExplicitMemorySlot ? params.pluginsConfig.slots.memory : undefined;
|
|
if (typeof memorySlot === "string") {
|
|
target.add(params.normalizePluginId(memorySlot));
|
|
}
|
|
const hasExplicitContextEngineSlot = Object.hasOwn(rawSlots, "contextEngine");
|
|
const contextEngineSlot = hasExplicitContextEngineSlot
|
|
? params.pluginsConfig.slots.contextEngine
|
|
: undefined;
|
|
if (typeof contextEngineSlot === "string" && contextEngineSlot !== "legacy") {
|
|
target.add(params.normalizePluginId(contextEngineSlot));
|
|
}
|
|
}
|
|
|
|
export function resolveConfigValidationMetadataPluginIds(params: {
|
|
config: OpenClawConfig;
|
|
env: NodeJS.ProcessEnv;
|
|
index: InstalledPluginIndex;
|
|
platform?: NodeJS.Platform;
|
|
}): string[] | undefined {
|
|
const lookup = createInstalledPluginIndexScopeLookup(params.index);
|
|
const pluginsConfig = normalizePluginsConfigForInstalledIndex(params.config.plugins, lookup);
|
|
if (params.config.plugins?.bundledDiscovery === "compat" || pluginsConfig.loadPaths.length > 0) {
|
|
return undefined;
|
|
}
|
|
|
|
const scope = new Set<string>();
|
|
addValidationPluginConfigReferences(scope, {
|
|
config: params.config,
|
|
pluginsConfig,
|
|
normalizePluginId: lookup.normalizePluginId,
|
|
});
|
|
if (!lookup.hasCompleteConfigPathActivationMetadata()) {
|
|
return undefined;
|
|
}
|
|
addConfiguredActivationPathPluginIds(scope, {
|
|
activationSourceConfig: params.config,
|
|
index: params.index,
|
|
});
|
|
|
|
const configuredChannelIds = collectConfigValidationChannelIds({
|
|
config: params.config,
|
|
env: params.env,
|
|
});
|
|
if (!lookup.hasChannelContributionOwners(configuredChannelIds)) {
|
|
return undefined;
|
|
}
|
|
lookup.addChannelContributionOwners(scope, configuredChannelIds);
|
|
|
|
const configuredProviderIds = collectValidationConfiguredProviderIds(params.config);
|
|
if (!lookup.hasProviderContributionOwners(configuredProviderIds)) {
|
|
return undefined;
|
|
}
|
|
lookup.addProviderContributionOwners(scope, configuredProviderIds);
|
|
|
|
const configuredShorthandModelIds = collectValidationConfiguredShorthandModelIds(params.config);
|
|
if (!lookup.hasShorthandModelOwners(configuredShorthandModelIds)) {
|
|
return undefined;
|
|
}
|
|
lookup.addShorthandModelOwners(scope, configuredShorthandModelIds);
|
|
|
|
addRequiredAgentHarnessPluginIds(scope, {
|
|
activationSourceConfig: params.config,
|
|
config: params.config,
|
|
index: params.index,
|
|
pluginsConfig,
|
|
activationSource: {
|
|
plugins: pluginsConfig,
|
|
rootConfig: params.config,
|
|
},
|
|
env: params.env,
|
|
platform: params.platform,
|
|
});
|
|
|
|
if (!lookup.hasInstalledPluginIds(scope)) {
|
|
return undefined;
|
|
}
|
|
return sortUniquePluginIds(scope);
|
|
}
|
|
|
|
export function createConfigValidationMetadataPluginIdScope(params: {
|
|
config: OpenClawConfig;
|
|
env: NodeJS.ProcessEnv;
|
|
platform?: NodeJS.Platform;
|
|
}): PluginMetadataSnapshotPluginIdScope {
|
|
const configuredChannelIds = collectConfigValidationChannelIds({
|
|
config: params.config,
|
|
env: params.env,
|
|
});
|
|
const configuredProviderIds = collectValidationConfiguredProviderIds(params.config);
|
|
const configuredShorthandModelIds = collectValidationConfiguredShorthandModelIds(params.config);
|
|
return {
|
|
key: hashJson({
|
|
kind: "config-validation",
|
|
config: params.config,
|
|
configuredChannelIds,
|
|
configuredProviderIds,
|
|
configuredShorthandModelIds,
|
|
platform: params.platform ?? null,
|
|
}),
|
|
resolve: ({ index }) =>
|
|
resolveConfigValidationMetadataPluginIds({
|
|
config: params.config,
|
|
env: params.env,
|
|
index,
|
|
...(params.platform !== undefined ? { platform: params.platform } : {}),
|
|
}),
|
|
};
|
|
}
|
|
|
|
export function isMetadataSnapshotScopedForGatewayStartup(params: {
|
|
metadataSnapshot: Pick<PluginMetadataSnapshot, "index" | "pluginIds">;
|
|
pluginIdScope: PluginMetadataSnapshotPluginIdScope;
|
|
}): boolean {
|
|
const expectedPluginIds = normalizePluginIdScope(
|
|
params.pluginIdScope.resolve({ index: params.metadataSnapshot.index }),
|
|
);
|
|
const snapshotPluginIds = normalizePluginIdScope(params.metadataSnapshot.pluginIds);
|
|
if (expectedPluginIds === undefined || snapshotPluginIds === undefined) {
|
|
return expectedPluginIds === undefined && snapshotPluginIds === undefined;
|
|
}
|
|
if (expectedPluginIds.length === 0) {
|
|
return snapshotPluginIds.length === 0;
|
|
}
|
|
const snapshotPluginIdSet = new Set(snapshotPluginIds);
|
|
return expectedPluginIds.every((pluginId) => snapshotPluginIdSet.has(pluginId));
|
|
}
|
|
|
|
function manifestOwnsConfiguredGenerationProvider(params: {
|
|
manifest: PluginManifestRecord | undefined;
|
|
configuredGenerationProviderIds: ConfiguredGenerationProviderIds;
|
|
}): boolean {
|
|
for (const contractKey of [
|
|
"imageGenerationProviders",
|
|
"videoGenerationProviders",
|
|
"musicGenerationProviders",
|
|
] as const) {
|
|
const configuredProviderIds = params.configuredGenerationProviderIds[contractKey];
|
|
if (configuredProviderIds.size === 0) {
|
|
continue;
|
|
}
|
|
if (
|
|
(params.manifest?.contracts?.[contractKey] ?? []).some((providerId) => {
|
|
const normalized = normalizeOptionalLowercaseString(providerId);
|
|
return normalized ? configuredProviderIds.has(normalized) : false;
|
|
})
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function manifestOwnsConfiguredVoiceProvider(params: {
|
|
manifest: PluginManifestRecord | undefined;
|
|
configuredVoiceProviderIds: ConfiguredVoiceProviderIds;
|
|
}): boolean {
|
|
for (const contractKey of [
|
|
"speechProviders",
|
|
"realtimeTranscriptionProviders",
|
|
"realtimeVoiceProviders",
|
|
] as const) {
|
|
const configuredProviderIds = params.configuredVoiceProviderIds[contractKey];
|
|
if (configuredProviderIds.size === 0) {
|
|
continue;
|
|
}
|
|
if (
|
|
(params.manifest?.contracts?.[contractKey] ?? []).some((providerId) => {
|
|
const normalized = normalizeOptionalLowercaseString(providerId);
|
|
return normalized ? configuredProviderIds.has(normalized) : false;
|
|
})
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function manifestOwnsConfiguredMemoryEmbeddingProvider(params: {
|
|
manifest: PluginManifestRecord | undefined;
|
|
configuredMemoryEmbeddingProviderIds: ReadonlySet<string>;
|
|
}): boolean {
|
|
if (params.configuredMemoryEmbeddingProviderIds.size === 0) {
|
|
return false;
|
|
}
|
|
const embeddingProviderIds = [
|
|
...(params.manifest?.contracts?.memoryEmbeddingProviders ?? []),
|
|
...(params.manifest?.contracts?.embeddingProviders ?? []),
|
|
];
|
|
return embeddingProviderIds.some((providerId) => {
|
|
const normalized = normalizeOptionalLowercaseString(providerId);
|
|
return normalized ? params.configuredMemoryEmbeddingProviderIds.has(normalized) : false;
|
|
});
|
|
}
|
|
|
|
function canStartConfiguredGenerationProviderPlugin(params: {
|
|
plugin: InstalledPluginIndexRecord;
|
|
manifest: PluginManifestRecord | undefined;
|
|
config: OpenClawConfig;
|
|
pluginsConfig: ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
|
activationSource: {
|
|
plugins: ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
|
rootConfig?: OpenClawConfig;
|
|
};
|
|
configuredGenerationProviderIds: ConfiguredGenerationProviderIds;
|
|
platform?: NodeJS.Platform;
|
|
}): boolean {
|
|
if (
|
|
!manifestOwnsConfiguredGenerationProvider({
|
|
manifest: params.manifest,
|
|
configuredGenerationProviderIds: params.configuredGenerationProviderIds,
|
|
})
|
|
) {
|
|
return false;
|
|
}
|
|
if (!params.pluginsConfig.enabled || !params.activationSource.plugins.enabled) {
|
|
return false;
|
|
}
|
|
if (
|
|
params.pluginsConfig.deny.includes(params.plugin.pluginId) ||
|
|
params.activationSource.plugins.deny.includes(params.plugin.pluginId)
|
|
) {
|
|
return false;
|
|
}
|
|
if (
|
|
params.pluginsConfig.entries[params.plugin.pluginId]?.enabled === false ||
|
|
params.activationSource.plugins.entries[params.plugin.pluginId]?.enabled === false
|
|
) {
|
|
return false;
|
|
}
|
|
const activationState = resolveEffectivePluginActivationState({
|
|
id: params.plugin.pluginId,
|
|
origin: params.plugin.origin,
|
|
config: params.pluginsConfig,
|
|
rootConfig: params.config,
|
|
enabledByDefault: isPluginEnabledByDefaultForPlatform(params.plugin, params.platform),
|
|
activationSource: params.activationSource,
|
|
});
|
|
return (
|
|
activationState.enabled &&
|
|
(params.plugin.origin === "bundled" || activationState.explicitlyEnabled)
|
|
);
|
|
}
|
|
|
|
function canStartConfiguredVoiceProviderPlugin(params: {
|
|
plugin: InstalledPluginIndexRecord;
|
|
manifest: PluginManifestRecord | undefined;
|
|
config: OpenClawConfig;
|
|
pluginsConfig: ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
|
activationSource: {
|
|
plugins: ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
|
rootConfig?: OpenClawConfig;
|
|
};
|
|
configuredVoiceProviderIds: ConfiguredVoiceProviderIds;
|
|
platform?: NodeJS.Platform;
|
|
}): boolean {
|
|
if (
|
|
!manifestOwnsConfiguredVoiceProvider({
|
|
manifest: params.manifest,
|
|
configuredVoiceProviderIds: params.configuredVoiceProviderIds,
|
|
})
|
|
) {
|
|
return false;
|
|
}
|
|
if (!params.pluginsConfig.enabled || !params.activationSource.plugins.enabled) {
|
|
return false;
|
|
}
|
|
if (
|
|
params.pluginsConfig.deny.includes(params.plugin.pluginId) ||
|
|
params.activationSource.plugins.deny.includes(params.plugin.pluginId)
|
|
) {
|
|
return false;
|
|
}
|
|
if (
|
|
params.pluginsConfig.entries[params.plugin.pluginId]?.enabled === false ||
|
|
params.activationSource.plugins.entries[params.plugin.pluginId]?.enabled === false
|
|
) {
|
|
return false;
|
|
}
|
|
const activationState = resolveEffectivePluginActivationState({
|
|
id: params.plugin.pluginId,
|
|
origin: params.plugin.origin,
|
|
config: params.pluginsConfig,
|
|
rootConfig: params.config,
|
|
enabledByDefault: isPluginEnabledByDefaultForPlatform(params.plugin, params.platform),
|
|
activationSource: params.activationSource,
|
|
});
|
|
return (
|
|
activationState.enabled &&
|
|
(params.plugin.origin === "bundled" || activationState.explicitlyEnabled)
|
|
);
|
|
}
|
|
|
|
function canStartConfiguredMemoryEmbeddingProviderPlugin(params: {
|
|
plugin: InstalledPluginIndexRecord;
|
|
manifest: PluginManifestRecord | undefined;
|
|
config: OpenClawConfig;
|
|
pluginsConfig: ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
|
activationSource: {
|
|
plugins: ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
|
rootConfig?: OpenClawConfig;
|
|
};
|
|
configuredMemoryEmbeddingProviderIds: ReadonlySet<string>;
|
|
platform?: NodeJS.Platform;
|
|
}): boolean {
|
|
if (
|
|
!manifestOwnsConfiguredMemoryEmbeddingProvider({
|
|
manifest: params.manifest,
|
|
configuredMemoryEmbeddingProviderIds: params.configuredMemoryEmbeddingProviderIds,
|
|
})
|
|
) {
|
|
return false;
|
|
}
|
|
if (!params.pluginsConfig.enabled || !params.activationSource.plugins.enabled) {
|
|
return false;
|
|
}
|
|
if (
|
|
params.pluginsConfig.deny.includes(params.plugin.pluginId) ||
|
|
params.activationSource.plugins.deny.includes(params.plugin.pluginId)
|
|
) {
|
|
return false;
|
|
}
|
|
if (
|
|
params.pluginsConfig.entries[params.plugin.pluginId]?.enabled === false ||
|
|
params.activationSource.plugins.entries[params.plugin.pluginId]?.enabled === false
|
|
) {
|
|
return false;
|
|
}
|
|
const activationState = resolveEffectivePluginActivationState({
|
|
id: params.plugin.pluginId,
|
|
origin: params.plugin.origin,
|
|
config: params.pluginsConfig,
|
|
rootConfig: params.config,
|
|
enabledByDefault: isPluginEnabledByDefaultForPlatform(params.plugin, params.platform),
|
|
activationSource: params.activationSource,
|
|
});
|
|
return activationState.enabled;
|
|
}
|
|
|
|
function canStartConfiguredModelProviderPlugin(params: {
|
|
plugin: InstalledPluginIndexRecord;
|
|
manifest: PluginManifestRecord | undefined;
|
|
config: OpenClawConfig;
|
|
pluginsConfig: ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
|
activationSource: {
|
|
plugins: ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
|
rootConfig?: OpenClawConfig;
|
|
};
|
|
configuredModelProviderIds: ReadonlySet<string>;
|
|
platform?: NodeJS.Platform;
|
|
}): boolean {
|
|
if (
|
|
!manifestOwnsConfiguredModelProvider({
|
|
manifest: params.manifest,
|
|
configuredModelProviderIds: params.configuredModelProviderIds,
|
|
})
|
|
) {
|
|
return false;
|
|
}
|
|
if (!params.pluginsConfig.enabled || !params.activationSource.plugins.enabled) {
|
|
return false;
|
|
}
|
|
if (
|
|
params.pluginsConfig.deny.includes(params.plugin.pluginId) ||
|
|
params.activationSource.plugins.deny.includes(params.plugin.pluginId)
|
|
) {
|
|
return false;
|
|
}
|
|
if (
|
|
params.pluginsConfig.entries[params.plugin.pluginId]?.enabled === false ||
|
|
params.activationSource.plugins.entries[params.plugin.pluginId]?.enabled === false
|
|
) {
|
|
return false;
|
|
}
|
|
const activationState = resolveEffectivePluginActivationState({
|
|
id: params.plugin.pluginId,
|
|
origin: params.plugin.origin,
|
|
config: params.pluginsConfig,
|
|
rootConfig: params.config,
|
|
enabledByDefault: isPluginEnabledByDefaultForPlatform(params.plugin, params.platform),
|
|
activationSource: params.activationSource,
|
|
});
|
|
return (
|
|
activationState.enabled &&
|
|
(params.plugin.origin === "bundled" || activationState.explicitlyEnabled)
|
|
);
|
|
}
|
|
|
|
function canStartRequiredAgentHarnessPlugin(params: {
|
|
plugin: InstalledPluginIndexRecord;
|
|
pluginsConfig: ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
|
activationSource: {
|
|
plugins: ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
|
rootConfig?: OpenClawConfig;
|
|
};
|
|
config: OpenClawConfig;
|
|
requiredAgentHarnessRuntimes: ReadonlySet<string>;
|
|
platform?: NodeJS.Platform;
|
|
}): boolean {
|
|
if (
|
|
!params.plugin.startup.agentHarnesses.some((runtime) =>
|
|
params.requiredAgentHarnessRuntimes.has(runtime),
|
|
)
|
|
) {
|
|
return false;
|
|
}
|
|
if (!params.pluginsConfig.enabled || !params.activationSource.plugins.enabled) {
|
|
return false;
|
|
}
|
|
if (
|
|
params.pluginsConfig.deny.includes(params.plugin.pluginId) ||
|
|
params.activationSource.plugins.deny.includes(params.plugin.pluginId)
|
|
) {
|
|
return false;
|
|
}
|
|
if (
|
|
params.pluginsConfig.entries[params.plugin.pluginId]?.enabled === false ||
|
|
params.activationSource.plugins.entries[params.plugin.pluginId]?.enabled === false
|
|
) {
|
|
return false;
|
|
}
|
|
if (
|
|
params.pluginsConfig.allow.length > 0 &&
|
|
!params.pluginsConfig.allow.includes(params.plugin.pluginId)
|
|
) {
|
|
return false;
|
|
}
|
|
if (
|
|
params.activationSource.plugins.allow.length > 0 &&
|
|
!params.activationSource.plugins.allow.includes(params.plugin.pluginId)
|
|
) {
|
|
return false;
|
|
}
|
|
const activationState = resolveEffectivePluginActivationState({
|
|
id: params.plugin.pluginId,
|
|
origin: params.plugin.origin,
|
|
config: params.pluginsConfig,
|
|
rootConfig: params.config,
|
|
enabledByDefault: isPluginEnabledByDefaultForPlatform(params.plugin, params.platform),
|
|
activationSource: params.activationSource,
|
|
});
|
|
return activationState.enabled || params.plugin.origin === "bundled";
|
|
}
|
|
|
|
function canStartConfiguredSpeechProviderPlugin(params: {
|
|
plugin: InstalledPluginIndexRecord;
|
|
manifest: PluginManifestRecord | undefined;
|
|
config: OpenClawConfig;
|
|
pluginsConfig: ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
|
activationSource: {
|
|
plugins: ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
|
rootConfig?: OpenClawConfig;
|
|
};
|
|
configuredSpeechProviderIds: ReadonlySet<string>;
|
|
platform?: NodeJS.Platform;
|
|
}): boolean {
|
|
if (
|
|
!manifestOwnsConfiguredSpeechProvider({
|
|
manifest: params.manifest,
|
|
configuredSpeechProviderIds: params.configuredSpeechProviderIds,
|
|
})
|
|
) {
|
|
return false;
|
|
}
|
|
if (
|
|
params.pluginsConfig.deny.includes(params.plugin.pluginId) ||
|
|
params.activationSource.plugins.deny.includes(params.plugin.pluginId)
|
|
) {
|
|
return false;
|
|
}
|
|
if (
|
|
params.pluginsConfig.entries[params.plugin.pluginId]?.enabled === false ||
|
|
params.activationSource.plugins.entries[params.plugin.pluginId]?.enabled === false
|
|
) {
|
|
return false;
|
|
}
|
|
if (params.plugin.origin === "bundled") {
|
|
return true;
|
|
}
|
|
const activationState = resolveEffectivePluginActivationState({
|
|
id: params.plugin.pluginId,
|
|
origin: params.plugin.origin,
|
|
config: params.pluginsConfig,
|
|
rootConfig: params.config,
|
|
enabledByDefault: isPluginEnabledByDefaultForPlatform(params.plugin, params.platform),
|
|
activationSource: params.activationSource,
|
|
});
|
|
return activationState.enabled && activationState.explicitlyEnabled;
|
|
}
|
|
|
|
function canStartConfiguredWebSearchProviderPlugin(params: {
|
|
plugin: InstalledPluginIndexRecord;
|
|
manifest: PluginManifestRecord | undefined;
|
|
config: OpenClawConfig;
|
|
pluginsConfig: ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
|
activationSource: {
|
|
plugins: ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
|
rootConfig?: OpenClawConfig;
|
|
};
|
|
configuredWebSearchProviderIds: ReadonlySet<string>;
|
|
platform?: NodeJS.Platform;
|
|
}): boolean {
|
|
if (
|
|
!manifestOwnsConfiguredWebSearchProvider({
|
|
manifest: params.manifest,
|
|
configuredWebSearchProviderIds: params.configuredWebSearchProviderIds,
|
|
})
|
|
) {
|
|
return false;
|
|
}
|
|
if (!params.pluginsConfig.enabled || !params.activationSource.plugins.enabled) {
|
|
return false;
|
|
}
|
|
if (
|
|
params.pluginsConfig.deny.includes(params.plugin.pluginId) ||
|
|
params.activationSource.plugins.deny.includes(params.plugin.pluginId)
|
|
) {
|
|
return false;
|
|
}
|
|
if (
|
|
params.pluginsConfig.entries[params.plugin.pluginId]?.enabled === false ||
|
|
params.activationSource.plugins.entries[params.plugin.pluginId]?.enabled === false
|
|
) {
|
|
return false;
|
|
}
|
|
const activationState = resolveEffectivePluginActivationState({
|
|
id: params.plugin.pluginId,
|
|
origin: params.plugin.origin,
|
|
config: params.pluginsConfig,
|
|
rootConfig: params.config,
|
|
enabledByDefault: isPluginEnabledByDefaultForPlatform(params.plugin, params.platform),
|
|
activationSource: params.activationSource,
|
|
});
|
|
return activationState.enabled;
|
|
}
|
|
|
|
function canStartConfiguredRootPlugin(params: {
|
|
plugin: InstalledPluginIndexRecord;
|
|
manifest: PluginManifestRecord | undefined;
|
|
config: OpenClawConfig;
|
|
pluginsConfig: ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
|
activationSourcePlugins: ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
|
}): boolean {
|
|
if (params.plugin.origin !== "bundled") {
|
|
return false;
|
|
}
|
|
if (!hasConfiguredActivationPath({ manifest: params.manifest, config: params.config })) {
|
|
return false;
|
|
}
|
|
if (!params.pluginsConfig.enabled || !params.activationSourcePlugins.enabled) {
|
|
return false;
|
|
}
|
|
if (
|
|
params.pluginsConfig.deny.includes(params.plugin.pluginId) ||
|
|
params.activationSourcePlugins.deny.includes(params.plugin.pluginId)
|
|
) {
|
|
return false;
|
|
}
|
|
if (
|
|
params.pluginsConfig.entries[params.plugin.pluginId]?.enabled === false ||
|
|
params.activationSourcePlugins.entries[params.plugin.pluginId]?.enabled === false
|
|
) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function hasExplicitHookPolicyConfig(
|
|
entry: NormalizedPluginsConfig["entries"][string] | undefined,
|
|
): boolean {
|
|
return (
|
|
entry?.hooks?.allowConversationAccess === true ||
|
|
entry?.hooks?.allowPromptInjection === true ||
|
|
entry?.hooks?.timeoutMs !== undefined ||
|
|
(entry?.hooks?.timeouts !== undefined && Object.keys(entry.hooks.timeouts).length > 0)
|
|
);
|
|
}
|
|
|
|
function hasHookRuntimeStartupIntent(params: {
|
|
plugin: InstalledPluginIndexRecord;
|
|
manifest: PluginManifestRecord | undefined;
|
|
activationSourcePlugins: NormalizedPluginsConfig;
|
|
}): boolean {
|
|
if (params.manifest?.activation?.onCapabilities?.includes("hook")) {
|
|
return true;
|
|
}
|
|
return hasExplicitHookPolicyConfig(
|
|
params.activationSourcePlugins.entries[params.plugin.pluginId],
|
|
);
|
|
}
|
|
|
|
function canStartExplicitHookPlugin(params: {
|
|
plugin: InstalledPluginIndexRecord;
|
|
manifest: PluginManifestRecord | undefined;
|
|
config: OpenClawConfig;
|
|
pluginsConfig: NormalizedPluginsConfig;
|
|
activationSource: {
|
|
plugins: NormalizedPluginsConfig;
|
|
rootConfig?: OpenClawConfig;
|
|
};
|
|
activationSourcePlugins: NormalizedPluginsConfig;
|
|
platform?: NodeJS.Platform;
|
|
}): boolean {
|
|
const hasHookPolicyIntent = hasExplicitHookPolicyConfig(
|
|
params.activationSourcePlugins.entries[params.plugin.pluginId],
|
|
);
|
|
if (
|
|
!hasHookRuntimeStartupIntent({
|
|
plugin: params.plugin,
|
|
manifest: params.manifest,
|
|
activationSourcePlugins: params.activationSourcePlugins,
|
|
})
|
|
) {
|
|
return false;
|
|
}
|
|
if (!params.pluginsConfig.enabled || !params.activationSourcePlugins.enabled) {
|
|
return false;
|
|
}
|
|
if (
|
|
params.pluginsConfig.deny.includes(params.plugin.pluginId) ||
|
|
params.activationSourcePlugins.deny.includes(params.plugin.pluginId)
|
|
) {
|
|
return false;
|
|
}
|
|
if (
|
|
params.pluginsConfig.entries[params.plugin.pluginId]?.enabled === false ||
|
|
params.activationSourcePlugins.entries[params.plugin.pluginId]?.enabled === false
|
|
) {
|
|
return false;
|
|
}
|
|
const activationState = resolveEffectivePluginActivationState({
|
|
id: params.plugin.pluginId,
|
|
origin: params.plugin.origin,
|
|
config: params.pluginsConfig,
|
|
rootConfig: params.config,
|
|
enabledByDefault: isPluginEnabledByDefaultForPlatform(params.plugin, params.platform),
|
|
activationSource: params.activationSource,
|
|
});
|
|
return activationState.enabled && (activationState.explicitlyEnabled || hasHookPolicyIntent);
|
|
}
|
|
|
|
function canStartTrustedToolPolicyPlugin(params: {
|
|
plugin: InstalledPluginIndexRecord;
|
|
manifest: PluginManifestRecord | undefined;
|
|
config: OpenClawConfig;
|
|
pluginsConfig: NormalizedPluginsConfig;
|
|
activationSource: {
|
|
plugins: NormalizedPluginsConfig;
|
|
rootConfig?: OpenClawConfig;
|
|
};
|
|
platform?: NodeJS.Platform;
|
|
}): boolean {
|
|
if ((params.manifest?.contracts?.trustedToolPolicies?.length ?? 0) === 0) {
|
|
return false;
|
|
}
|
|
if (!params.pluginsConfig.enabled || !params.activationSource.plugins.enabled) {
|
|
return false;
|
|
}
|
|
if (
|
|
params.pluginsConfig.deny.includes(params.plugin.pluginId) ||
|
|
params.activationSource.plugins.deny.includes(params.plugin.pluginId)
|
|
) {
|
|
return false;
|
|
}
|
|
if (
|
|
params.pluginsConfig.entries[params.plugin.pluginId]?.enabled === false ||
|
|
params.activationSource.plugins.entries[params.plugin.pluginId]?.enabled === false
|
|
) {
|
|
return false;
|
|
}
|
|
const activationState = resolveEffectivePluginActivationState({
|
|
id: params.plugin.pluginId,
|
|
origin: params.plugin.origin,
|
|
config: params.pluginsConfig,
|
|
rootConfig: params.config,
|
|
enabledByDefault: isPluginEnabledByDefaultForPlatform(params.plugin, params.platform),
|
|
activationSource: params.activationSource,
|
|
});
|
|
return (
|
|
activationState.enabled &&
|
|
(params.plugin.origin === "bundled" || activationState.explicitlyEnabled)
|
|
);
|
|
}
|
|
|
|
function canStartConfiguredChannelPlugin(params: {
|
|
plugin: InstalledPluginIndexRecord;
|
|
config: OpenClawConfig;
|
|
pluginsConfig: ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
|
activationSource: {
|
|
plugins: ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
|
rootConfig?: OpenClawConfig;
|
|
};
|
|
manifestLookup: ManifestRegistryLookup;
|
|
platform?: NodeJS.Platform;
|
|
}): boolean {
|
|
if (!params.pluginsConfig.enabled) {
|
|
return false;
|
|
}
|
|
if (params.pluginsConfig.deny.includes(params.plugin.pluginId)) {
|
|
return false;
|
|
}
|
|
if (params.pluginsConfig.entries[params.plugin.pluginId]?.enabled === false) {
|
|
return false;
|
|
}
|
|
const explicitBundledChannelConfig =
|
|
params.plugin.origin === "bundled" &&
|
|
listManifestChannelIds(params.manifestLookup, params.plugin.pluginId).some((channelId) =>
|
|
hasExplicitChannelConfig({
|
|
config: params.activationSource.rootConfig ?? params.config,
|
|
channelId,
|
|
}),
|
|
);
|
|
if (
|
|
params.pluginsConfig.allow.length > 0 &&
|
|
!params.pluginsConfig.allow.includes(params.plugin.pluginId) &&
|
|
!explicitBundledChannelConfig
|
|
) {
|
|
return false;
|
|
}
|
|
if (params.plugin.origin === "bundled") {
|
|
return true;
|
|
}
|
|
const activationState = resolveEffectivePluginActivationState({
|
|
id: params.plugin.pluginId,
|
|
origin: params.plugin.origin,
|
|
config: params.pluginsConfig,
|
|
rootConfig: params.config,
|
|
enabledByDefault: isPluginEnabledByDefaultForPlatform(params.plugin, params.platform),
|
|
activationSource: params.activationSource,
|
|
});
|
|
return activationState.enabled && activationState.explicitlyEnabled;
|
|
}
|
|
|
|
export function resolveChannelPluginIds(params: {
|
|
config: OpenClawConfig;
|
|
workspaceDir?: string;
|
|
env: NodeJS.ProcessEnv;
|
|
}): string[] {
|
|
return [...loadGatewayStartupPluginPlan(params).channelPluginIds];
|
|
}
|
|
|
|
export function resolveChannelPluginIdsFromRegistry(params: {
|
|
manifestRegistry: PluginManifestRegistry;
|
|
}): string[] {
|
|
const { manifestRegistry } = params;
|
|
return manifestRegistry.plugins
|
|
.filter((plugin) => plugin.channels.length > 0)
|
|
.map((plugin) => plugin.id);
|
|
}
|
|
|
|
export function resolveConfiguredDeferredChannelPluginIdsFromRegistry(params: {
|
|
config: OpenClawConfig;
|
|
env: NodeJS.ProcessEnv;
|
|
index: PluginRegistrySnapshot;
|
|
manifestRegistry: PluginManifestRegistry;
|
|
}): string[] {
|
|
const configuredChannelIds = new Set(listPotentialEnabledChannelIds(params.config, params.env));
|
|
if (configuredChannelIds.size === 0) {
|
|
return [];
|
|
}
|
|
const pluginsConfig = normalizePluginsConfigWithRegistry(params.config.plugins, params.index, {
|
|
manifestRegistry: params.manifestRegistry,
|
|
});
|
|
const activationSource = {
|
|
plugins: pluginsConfig,
|
|
rootConfig: params.config,
|
|
};
|
|
const manifestLookup = createManifestRegistryLookup(params.manifestRegistry);
|
|
return resolveConfiguredDeferredChannelPluginIdsFromPrepared({
|
|
config: params.config,
|
|
index: params.index,
|
|
configuredChannelIds,
|
|
pluginsConfig,
|
|
activationSource,
|
|
manifestLookup,
|
|
});
|
|
}
|
|
|
|
function resolveConfiguredDeferredChannelPluginIdsFromPrepared(params: {
|
|
config: OpenClawConfig;
|
|
index: PluginRegistrySnapshot;
|
|
configuredChannelIds: ReadonlySet<string>;
|
|
pluginsConfig: ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
|
activationSource: {
|
|
plugins: ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
|
rootConfig?: OpenClawConfig;
|
|
};
|
|
manifestLookup: ManifestRegistryLookup;
|
|
platform?: NodeJS.Platform;
|
|
}): string[] {
|
|
if (params.configuredChannelIds.size === 0) {
|
|
return [];
|
|
}
|
|
return params.index.plugins
|
|
.filter(
|
|
(plugin) =>
|
|
hasConfiguredStartupChannel({
|
|
plugin,
|
|
manifestLookup: params.manifestLookup,
|
|
configuredChannelIds: params.configuredChannelIds,
|
|
}) &&
|
|
plugin.startup.deferConfiguredChannelFullLoadUntilAfterListen &&
|
|
canStartConfiguredChannelPlugin({
|
|
plugin,
|
|
config: params.config,
|
|
pluginsConfig: params.pluginsConfig,
|
|
activationSource: params.activationSource,
|
|
manifestLookup: params.manifestLookup,
|
|
platform: params.platform,
|
|
}),
|
|
)
|
|
.map((plugin) => plugin.pluginId);
|
|
}
|
|
|
|
export function resolveConfiguredDeferredChannelPluginIds(params: {
|
|
config: OpenClawConfig;
|
|
workspaceDir?: string;
|
|
env: NodeJS.ProcessEnv;
|
|
}): string[] {
|
|
return [...loadGatewayStartupPluginPlan(params).configuredDeferredChannelPluginIds];
|
|
}
|
|
|
|
export function resolveGatewayStartupPluginPlanFromRegistry(params: {
|
|
config: OpenClawConfig;
|
|
activationSourceConfig?: OpenClawConfig;
|
|
env: NodeJS.ProcessEnv;
|
|
index: PluginRegistrySnapshot;
|
|
manifestRegistry: PluginManifestRegistry;
|
|
platform?: NodeJS.Platform;
|
|
}): GatewayStartupPluginPlan {
|
|
const channelPluginIds = resolveChannelPluginIdsFromRegistry({
|
|
manifestRegistry: params.manifestRegistry,
|
|
});
|
|
const configuredChannelIds = new Set(listPotentialEnabledChannelIds(params.config, params.env));
|
|
const pluginsConfig = normalizePluginsConfigWithRegistry(params.config.plugins, params.index, {
|
|
manifestRegistry: params.manifestRegistry,
|
|
});
|
|
// Startup must classify allowlist exceptions against the raw config snapshot,
|
|
// not the auto-enabled effective snapshot, or configured-only channels can be
|
|
// misclassified as explicit enablement.
|
|
const activationSourceConfig = params.activationSourceConfig ?? params.config;
|
|
const activationSourcePlugins = normalizePluginsConfigWithRegistry(
|
|
activationSourceConfig.plugins,
|
|
params.index,
|
|
{ manifestRegistry: params.manifestRegistry },
|
|
);
|
|
const activationSource = {
|
|
plugins: activationSourcePlugins,
|
|
rootConfig: activationSourceConfig,
|
|
};
|
|
const manifestLookup = createManifestRegistryLookup(params.manifestRegistry);
|
|
const explicitlyDisabledChannelIds = new Set(
|
|
listExplicitlyDisabledChannelIdsForConfig(params.config),
|
|
);
|
|
const configuredDeferredChannelPluginIds: string[] = [];
|
|
const requiredAgentHarnessRuntimes = new Set(
|
|
collectConfiguredAgentHarnessRuntimes(activationSourceConfig),
|
|
);
|
|
const configuredSpeechProviderIds = collectConfiguredSpeechProviderIds(activationSourceConfig);
|
|
const configuredWebSearchProviderIds =
|
|
collectConfiguredWebSearchProviderIds(activationSourceConfig);
|
|
const configuredModelProviderIds = collectConfiguredAgentModelProviderIds(
|
|
activationSourceConfig,
|
|
params.manifestRegistry,
|
|
);
|
|
const configuredGenerationProviderIds =
|
|
collectConfiguredGenerationProviderIds(activationSourceConfig);
|
|
const configuredVoiceProviderIds = collectConfiguredVoiceProviderIds(activationSourceConfig);
|
|
const configuredMemoryEmbeddingProviderIds =
|
|
collectConfiguredMemoryEmbeddingProviderIds(activationSourceConfig);
|
|
const normalizePluginId = createPluginRegistryIdNormalizer(params.index, {
|
|
manifestRegistry: params.manifestRegistry,
|
|
});
|
|
const memorySlotStartupPluginId = resolveMemorySlotStartupPluginId({
|
|
activationSourceConfig,
|
|
activationSourcePlugins,
|
|
normalizePluginId,
|
|
});
|
|
const startupDreamingPluginIds = resolveAuthorizedGatewayStartupDreamingPluginIds({
|
|
config: params.config,
|
|
pluginsConfig,
|
|
activationSource,
|
|
activationSourcePlugins,
|
|
selectedMemoryPluginId: memorySlotStartupPluginId,
|
|
index: params.index,
|
|
platform: params.platform,
|
|
});
|
|
const contextEngineSlotStartupPluginId = resolveContextEngineSlotStartupPluginId({
|
|
activationSourceConfig,
|
|
activationSourcePlugins,
|
|
normalizePluginId,
|
|
});
|
|
const pluginIds: string[] = [];
|
|
for (const plugin of params.index.plugins) {
|
|
const manifest = findManifestPlugin(manifestLookup, plugin.pluginId);
|
|
const hasEnabledManifestChannel =
|
|
manifest?.channels?.some((channelId) => {
|
|
const normalizedChannelId = normalizeOptionalLowercaseString(channelId);
|
|
return normalizedChannelId ? !explicitlyDisabledChannelIds.has(normalizedChannelId) : false;
|
|
}) ?? false;
|
|
// Non-bundled plugin that explicitly declares channels and is enabled
|
|
// in plugins.entries must be treated as a configured startup channel
|
|
// even when the channel itself is not listed in config.channels.
|
|
// Published install flows configure channels via plugins.entries, and
|
|
// the channel config may only have {enabled: true} which does not
|
|
// produce a `configuredChannelIds` entry.
|
|
const hasExplicitlyEnabledNonBundledChannel =
|
|
plugin.origin !== "bundled" &&
|
|
hasEnabledManifestChannel &&
|
|
pluginsConfig.entries[plugin.pluginId]?.enabled === true &&
|
|
!pluginsConfig.deny.includes(plugin.pluginId);
|
|
if (
|
|
hasConfiguredStartupChannel({
|
|
plugin,
|
|
manifestLookup,
|
|
configuredChannelIds,
|
|
}) ||
|
|
hasExplicitlyEnabledNonBundledChannel
|
|
) {
|
|
const canStartConfiguredChannel = canStartConfiguredChannelPlugin({
|
|
plugin,
|
|
config: params.config,
|
|
pluginsConfig,
|
|
activationSource,
|
|
manifestLookup,
|
|
platform: params.platform,
|
|
});
|
|
if (canStartConfiguredChannel) {
|
|
pluginIds.push(plugin.pluginId);
|
|
if (plugin.startup.deferConfiguredChannelFullLoadUntilAfterListen) {
|
|
configuredDeferredChannelPluginIds.push(plugin.pluginId);
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
if (
|
|
canStartRequiredAgentHarnessPlugin({
|
|
plugin,
|
|
pluginsConfig,
|
|
activationSource,
|
|
config: params.config,
|
|
requiredAgentHarnessRuntimes,
|
|
platform: params.platform,
|
|
})
|
|
) {
|
|
pluginIds.push(plugin.pluginId);
|
|
continue;
|
|
}
|
|
if (
|
|
canStartConfiguredRootPlugin({
|
|
plugin,
|
|
manifest,
|
|
config: activationSourceConfig,
|
|
pluginsConfig,
|
|
activationSourcePlugins,
|
|
})
|
|
) {
|
|
pluginIds.push(plugin.pluginId);
|
|
continue;
|
|
}
|
|
if (
|
|
canStartConfiguredSpeechProviderPlugin({
|
|
plugin,
|
|
manifest,
|
|
config: params.config,
|
|
pluginsConfig,
|
|
activationSource,
|
|
configuredSpeechProviderIds,
|
|
platform: params.platform,
|
|
})
|
|
) {
|
|
pluginIds.push(plugin.pluginId);
|
|
continue;
|
|
}
|
|
if (
|
|
canStartConfiguredWebSearchProviderPlugin({
|
|
plugin,
|
|
manifest,
|
|
config: params.config,
|
|
pluginsConfig,
|
|
activationSource,
|
|
configuredWebSearchProviderIds,
|
|
platform: params.platform,
|
|
})
|
|
) {
|
|
pluginIds.push(plugin.pluginId);
|
|
continue;
|
|
}
|
|
if (
|
|
canStartConfiguredModelProviderPlugin({
|
|
plugin,
|
|
manifest,
|
|
config: params.config,
|
|
pluginsConfig,
|
|
activationSource,
|
|
configuredModelProviderIds,
|
|
platform: params.platform,
|
|
})
|
|
) {
|
|
pluginIds.push(plugin.pluginId);
|
|
continue;
|
|
}
|
|
if (
|
|
canStartConfiguredGenerationProviderPlugin({
|
|
plugin,
|
|
manifest,
|
|
config: params.config,
|
|
pluginsConfig,
|
|
activationSource,
|
|
configuredGenerationProviderIds,
|
|
platform: params.platform,
|
|
})
|
|
) {
|
|
pluginIds.push(plugin.pluginId);
|
|
continue;
|
|
}
|
|
if (
|
|
canStartConfiguredVoiceProviderPlugin({
|
|
plugin,
|
|
manifest,
|
|
config: params.config,
|
|
pluginsConfig,
|
|
activationSource,
|
|
configuredVoiceProviderIds,
|
|
platform: params.platform,
|
|
})
|
|
) {
|
|
pluginIds.push(plugin.pluginId);
|
|
continue;
|
|
}
|
|
if (
|
|
canStartConfiguredMemoryEmbeddingProviderPlugin({
|
|
plugin,
|
|
manifest,
|
|
config: params.config,
|
|
pluginsConfig,
|
|
activationSource,
|
|
configuredMemoryEmbeddingProviderIds,
|
|
platform: params.platform,
|
|
})
|
|
) {
|
|
pluginIds.push(plugin.pluginId);
|
|
continue;
|
|
}
|
|
if (
|
|
canStartExplicitHookPlugin({
|
|
plugin,
|
|
manifest,
|
|
config: params.config,
|
|
pluginsConfig,
|
|
activationSource,
|
|
activationSourcePlugins,
|
|
platform: params.platform,
|
|
})
|
|
) {
|
|
pluginIds.push(plugin.pluginId);
|
|
continue;
|
|
}
|
|
if (
|
|
canStartTrustedToolPolicyPlugin({
|
|
plugin,
|
|
manifest,
|
|
config: params.config,
|
|
pluginsConfig,
|
|
activationSource,
|
|
platform: params.platform,
|
|
})
|
|
) {
|
|
pluginIds.push(plugin.pluginId);
|
|
continue;
|
|
}
|
|
if (
|
|
!shouldConsiderForGatewayStartup({
|
|
plugin,
|
|
manifest,
|
|
startupDreamingPluginIds,
|
|
memorySlotStartupPluginId,
|
|
contextEngineSlotStartupPluginId,
|
|
})
|
|
) {
|
|
continue;
|
|
}
|
|
if (startupDreamingPluginIds.has(plugin.pluginId)) {
|
|
pluginIds.push(plugin.pluginId);
|
|
continue;
|
|
}
|
|
const activationState = resolveEffectivePluginActivationState({
|
|
id: plugin.pluginId,
|
|
origin: plugin.origin,
|
|
config: pluginsConfig,
|
|
rootConfig: params.config,
|
|
enabledByDefault: isPluginEnabledByDefaultForPlatform(plugin, params.platform),
|
|
activationSource,
|
|
});
|
|
if (!activationState.enabled) {
|
|
continue;
|
|
}
|
|
if (
|
|
plugin.origin !== "bundled"
|
|
? activationState.explicitlyEnabled
|
|
: activationState.source === "explicit" || activationState.source === "default"
|
|
) {
|
|
pluginIds.push(plugin.pluginId);
|
|
}
|
|
}
|
|
return {
|
|
channelPluginIds,
|
|
configuredDeferredChannelPluginIds,
|
|
pluginIds,
|
|
};
|
|
}
|
|
|
|
export function resolveGatewayStartupPluginIdsFromRegistry(params: {
|
|
config: OpenClawConfig;
|
|
activationSourceConfig?: OpenClawConfig;
|
|
env: NodeJS.ProcessEnv;
|
|
index: PluginRegistrySnapshot;
|
|
manifestRegistry: PluginManifestRegistry;
|
|
platform?: NodeJS.Platform;
|
|
}): string[] {
|
|
return [...resolveGatewayStartupPluginPlanFromRegistry(params).pluginIds];
|
|
}
|
|
|
|
export function loadGatewayStartupPluginPlan(params: {
|
|
config: OpenClawConfig;
|
|
activationSourceConfig?: OpenClawConfig;
|
|
workspaceDir?: string;
|
|
env: NodeJS.ProcessEnv;
|
|
index?: PluginRegistrySnapshot;
|
|
metadataSnapshot?: PluginMetadataSnapshot;
|
|
platform?: NodeJS.Platform;
|
|
}): GatewayStartupPluginPlan {
|
|
const snapshotConfig = params.activationSourceConfig ?? params.config;
|
|
const pluginIdScope = createGatewayStartupMetadataPluginIdScope({
|
|
config: params.config,
|
|
...(params.activationSourceConfig !== undefined
|
|
? { activationSourceConfig: params.activationSourceConfig }
|
|
: {}),
|
|
env: params.env,
|
|
...(params.platform !== undefined ? { platform: params.platform } : {}),
|
|
});
|
|
const metadataSnapshot =
|
|
params.metadataSnapshot &&
|
|
isPluginMetadataSnapshotCompatible({
|
|
snapshot: params.metadataSnapshot,
|
|
config: snapshotConfig,
|
|
env: params.env,
|
|
allowScopedSnapshot: true,
|
|
workspaceDir: params.workspaceDir,
|
|
index: params.index,
|
|
}) &&
|
|
isMetadataSnapshotScopedForGatewayStartup({
|
|
metadataSnapshot: params.metadataSnapshot,
|
|
pluginIdScope,
|
|
})
|
|
? params.metadataSnapshot
|
|
: resolvePluginMetadataSnapshot({
|
|
config: snapshotConfig,
|
|
workspaceDir: params.workspaceDir,
|
|
env: params.env,
|
|
allowWorkspaceScopedCurrent: params.workspaceDir === undefined,
|
|
...(params.index ? { index: params.index } : {}),
|
|
pluginIdScope,
|
|
});
|
|
return resolveGatewayStartupPluginPlanFromRegistry({
|
|
config: params.config,
|
|
...(params.activationSourceConfig !== undefined
|
|
? { activationSourceConfig: params.activationSourceConfig }
|
|
: {}),
|
|
env: params.env,
|
|
index: metadataSnapshot.index,
|
|
manifestRegistry: metadataSnapshot.manifestRegistry,
|
|
platform: params.platform,
|
|
});
|
|
}
|
|
|
|
export function resolveGatewayStartupPluginIds(params: {
|
|
config: OpenClawConfig;
|
|
activationSourceConfig?: OpenClawConfig;
|
|
workspaceDir?: string;
|
|
env: NodeJS.ProcessEnv;
|
|
platform?: NodeJS.Platform;
|
|
}): string[] {
|
|
return [...loadGatewayStartupPluginPlan(params).pluginIds];
|
|
}
|