Files
openclaw/src/plugins/config-state.ts
2026-04-06 17:56:41 +01:00

432 lines
12 KiB
TypeScript

import type { OpenClawConfig } from "../config/config.js";
import {
resolveMemorySlotDecisionShared,
resolveEnableStateResult,
} from "./config-activation-shared.js";
import {
hasExplicitPluginConfig as hasExplicitPluginConfigShared,
isBundledChannelEnabledByChannelConfig as isBundledChannelEnabledByChannelConfigShared,
normalizePluginsConfigWithResolver,
type NormalizedPluginsConfig as SharedNormalizedPluginsConfig,
} from "./config-normalization-shared.js";
import { loadPluginManifestRegistry } from "./manifest-registry.js";
import type { PluginOrigin } from "./types.js";
export type PluginActivationSource = "disabled" | "explicit" | "auto" | "default";
export type PluginExplicitSelectionCause =
| "enabled-in-config"
| "bundled-channel-enabled-in-config"
| "selected-memory-slot"
| "selected-in-allowlist";
export type PluginActivationCause =
| PluginExplicitSelectionCause
| "plugins-disabled"
| "blocked-by-denylist"
| "disabled-in-config"
| "workspace-disabled-by-default"
| "not-in-allowlist"
| "enabled-by-effective-config"
| "bundled-channel-configured"
| "bundled-default-enablement"
| "bundled-disabled-by-default";
export type PluginActivationState = {
enabled: boolean;
activated: boolean;
explicitlyEnabled: boolean;
source: PluginActivationSource;
reason?: string;
};
type PluginActivationDecision = {
enabled: boolean;
activated: boolean;
explicitlyEnabled: boolean;
source: PluginActivationSource;
cause?: PluginActivationCause;
reason?: string;
};
export type PluginActivationConfigSource = {
plugins: NormalizedPluginsConfig;
rootConfig?: OpenClawConfig;
};
export type NormalizedPluginsConfig = SharedNormalizedPluginsConfig;
let bundledPluginAliasLookupCache: ReadonlyMap<string, string> | undefined;
function getBundledPluginAliasLookup(): ReadonlyMap<string, string> {
if (bundledPluginAliasLookupCache) {
return bundledPluginAliasLookupCache;
}
const lookup = new Map<string, string>();
for (const plugin of loadPluginManifestRegistry({ cache: true }).plugins) {
if (plugin.origin !== "bundled") {
continue;
}
lookup.set(plugin.id.toLowerCase(), plugin.id);
for (const providerId of plugin.providers) {
lookup.set(providerId.toLowerCase(), plugin.id);
}
for (const legacyPluginId of plugin.legacyPluginIds ?? []) {
lookup.set(legacyPluginId.toLowerCase(), plugin.id);
}
}
bundledPluginAliasLookupCache = lookup;
return lookup;
}
export function normalizePluginId(id: string): string {
const trimmed = id.trim();
return getBundledPluginAliasLookup().get(trimmed.toLowerCase()) ?? trimmed;
}
const PLUGIN_ACTIVATION_REASON_BY_CAUSE: Record<PluginActivationCause, string> = {
"enabled-in-config": "enabled in config",
"bundled-channel-enabled-in-config": "channel enabled in config",
"selected-memory-slot": "selected memory slot",
"selected-in-allowlist": "selected in allowlist",
"plugins-disabled": "plugins disabled",
"blocked-by-denylist": "blocked by denylist",
"disabled-in-config": "disabled in config",
"workspace-disabled-by-default": "workspace plugin (disabled by default)",
"not-in-allowlist": "not in allowlist",
"enabled-by-effective-config": "enabled by effective config",
"bundled-channel-configured": "channel configured",
"bundled-default-enablement": "bundled default enablement",
"bundled-disabled-by-default": "bundled (disabled by default)",
};
function resolvePluginActivationReason(
cause?: PluginActivationCause,
reason?: string,
): string | undefined {
if (reason) {
return reason;
}
return cause ? PLUGIN_ACTIVATION_REASON_BY_CAUSE[cause] : undefined;
}
function toPluginActivationState(decision: PluginActivationDecision): PluginActivationState {
return {
enabled: decision.enabled,
activated: decision.activated,
explicitlyEnabled: decision.explicitlyEnabled,
source: decision.source,
reason: resolvePluginActivationReason(decision.cause, decision.reason),
};
}
export const normalizePluginsConfig = (
config?: OpenClawConfig["plugins"],
): NormalizedPluginsConfig => {
return normalizePluginsConfigWithResolver(config, normalizePluginId);
};
export function createPluginActivationSource(params: {
config?: OpenClawConfig;
plugins?: NormalizedPluginsConfig;
}): PluginActivationConfigSource {
return {
plugins: params.plugins ?? normalizePluginsConfig(params.config?.plugins),
rootConfig: params.config,
};
}
const hasExplicitMemorySlot = (plugins?: OpenClawConfig["plugins"]) =>
Boolean(plugins?.slots && Object.prototype.hasOwnProperty.call(plugins.slots, "memory"));
const hasExplicitMemoryEntry = (plugins?: OpenClawConfig["plugins"]) =>
Boolean(plugins?.entries && Object.prototype.hasOwnProperty.call(plugins.entries, "memory-core"));
export const hasExplicitPluginConfig = (plugins?: OpenClawConfig["plugins"]) =>
hasExplicitPluginConfigShared(plugins);
export function applyTestPluginDefaults(
cfg: OpenClawConfig,
env: NodeJS.ProcessEnv = process.env,
): OpenClawConfig {
if (!env.VITEST) {
return cfg;
}
const plugins = cfg.plugins;
const explicitConfig = hasExplicitPluginConfig(plugins);
if (explicitConfig) {
if (hasExplicitMemorySlot(plugins) || hasExplicitMemoryEntry(plugins)) {
return cfg;
}
return {
...cfg,
plugins: {
...plugins,
slots: {
...plugins?.slots,
memory: "none",
},
},
};
}
return {
...cfg,
plugins: {
...plugins,
enabled: false,
slots: {
...plugins?.slots,
memory: "none",
},
},
};
}
export function isTestDefaultMemorySlotDisabled(
cfg: OpenClawConfig,
env: NodeJS.ProcessEnv = process.env,
): boolean {
if (!env.VITEST) {
return false;
}
const plugins = cfg.plugins;
if (hasExplicitMemorySlot(plugins) || hasExplicitMemoryEntry(plugins)) {
return false;
}
return true;
}
function resolveExplicitPluginSelection(params: {
id: string;
origin: PluginOrigin;
config: NormalizedPluginsConfig;
rootConfig?: OpenClawConfig;
}): { explicitlyEnabled: boolean; cause?: PluginExplicitSelectionCause } {
if (params.config.entries[params.id]?.enabled === true) {
return { explicitlyEnabled: true, cause: "enabled-in-config" };
}
if (
params.origin === "bundled" &&
isBundledChannelEnabledByChannelConfig(params.rootConfig, params.id)
) {
return { explicitlyEnabled: true, cause: "bundled-channel-enabled-in-config" };
}
if (params.config.slots.memory === params.id) {
return { explicitlyEnabled: true, cause: "selected-memory-slot" };
}
if (params.origin !== "bundled" && params.config.allow.includes(params.id)) {
return { explicitlyEnabled: true, cause: "selected-in-allowlist" };
}
return { explicitlyEnabled: false };
}
export function resolvePluginActivationState(params: {
id: string;
origin: PluginOrigin;
config: NormalizedPluginsConfig;
rootConfig?: OpenClawConfig;
enabledByDefault?: boolean;
activationSource?: PluginActivationConfigSource;
autoEnabledReason?: string;
}): PluginActivationState {
const activationSource =
params.activationSource ??
createPluginActivationSource({
config: params.rootConfig,
plugins: params.config,
});
const explicitSelection = resolveExplicitPluginSelection({
id: params.id,
origin: params.origin,
config: activationSource.plugins,
rootConfig: activationSource.rootConfig,
});
if (!params.config.enabled) {
return toPluginActivationState({
enabled: false,
activated: false,
explicitlyEnabled: explicitSelection.explicitlyEnabled,
source: "disabled",
cause: "plugins-disabled",
});
}
if (params.config.deny.includes(params.id)) {
return toPluginActivationState({
enabled: false,
activated: false,
explicitlyEnabled: explicitSelection.explicitlyEnabled,
source: "disabled",
cause: "blocked-by-denylist",
});
}
const entry = params.config.entries[params.id];
if (entry?.enabled === false) {
return toPluginActivationState({
enabled: false,
activated: false,
explicitlyEnabled: explicitSelection.explicitlyEnabled,
source: "disabled",
cause: "disabled-in-config",
});
}
const explicitlyAllowed = params.config.allow.includes(params.id);
if (params.origin === "workspace" && !explicitlyAllowed && entry?.enabled !== true) {
return toPluginActivationState({
enabled: false,
activated: false,
explicitlyEnabled: explicitSelection.explicitlyEnabled,
source: "disabled",
cause: "workspace-disabled-by-default",
});
}
if (params.config.slots.memory === params.id) {
return toPluginActivationState({
enabled: true,
activated: true,
explicitlyEnabled: true,
source: "explicit",
cause: "selected-memory-slot",
});
}
if (explicitSelection.cause === "bundled-channel-enabled-in-config") {
return toPluginActivationState({
enabled: true,
activated: true,
explicitlyEnabled: true,
source: "explicit",
cause: explicitSelection.cause,
});
}
if (params.config.allow.length > 0 && !explicitlyAllowed) {
return toPluginActivationState({
enabled: false,
activated: false,
explicitlyEnabled: explicitSelection.explicitlyEnabled,
source: "disabled",
cause: "not-in-allowlist",
});
}
if (explicitSelection.explicitlyEnabled) {
return toPluginActivationState({
enabled: true,
activated: true,
explicitlyEnabled: true,
source: "explicit",
cause: explicitSelection.cause,
});
}
if (params.autoEnabledReason) {
return toPluginActivationState({
enabled: true,
activated: true,
explicitlyEnabled: false,
source: "auto",
reason: params.autoEnabledReason,
});
}
if (entry?.enabled === true) {
return toPluginActivationState({
enabled: true,
activated: true,
explicitlyEnabled: false,
source: "auto",
cause: "enabled-by-effective-config",
});
}
if (
params.origin === "bundled" &&
isBundledChannelEnabledByChannelConfig(params.rootConfig, params.id)
) {
return toPluginActivationState({
enabled: true,
activated: true,
explicitlyEnabled: false,
source: "auto",
cause: "bundled-channel-configured",
});
}
if (params.origin === "bundled" && params.enabledByDefault === true) {
return toPluginActivationState({
enabled: true,
activated: true,
explicitlyEnabled: false,
source: "default",
cause: "bundled-default-enablement",
});
}
if (params.origin === "bundled") {
return toPluginActivationState({
enabled: false,
activated: false,
explicitlyEnabled: false,
source: "disabled",
cause: "bundled-disabled-by-default",
});
}
return toPluginActivationState({
enabled: true,
activated: true,
explicitlyEnabled: explicitSelection.explicitlyEnabled,
source: "default",
});
}
export function resolveEnableState(
id: string,
origin: PluginOrigin,
config: NormalizedPluginsConfig,
enabledByDefault?: boolean,
): { enabled: boolean; reason?: string } {
return resolveEnableStateResult(
{
id,
origin,
config,
enabledByDefault,
},
resolvePluginActivationState,
);
}
export function isBundledChannelEnabledByChannelConfig(
cfg: OpenClawConfig | undefined,
pluginId: string,
): boolean {
return isBundledChannelEnabledByChannelConfigShared(cfg, pluginId);
}
export function resolveEffectiveEnableState(params: {
id: string;
origin: PluginOrigin;
config: NormalizedPluginsConfig;
rootConfig?: OpenClawConfig;
enabledByDefault?: boolean;
activationSource?: PluginActivationConfigSource;
}): { enabled: boolean; reason?: string } {
return resolveEnableStateResult(params, resolveEffectivePluginActivationState);
}
export function resolveEffectivePluginActivationState(params: {
id: string;
origin: PluginOrigin;
config: NormalizedPluginsConfig;
rootConfig?: OpenClawConfig;
enabledByDefault?: boolean;
activationSource?: PluginActivationConfigSource;
autoEnabledReason?: string;
}): PluginActivationState {
return resolvePluginActivationState(params);
}
export function resolveMemorySlotDecision(params: {
id: string;
kind?: string | string[];
slot: string | null | undefined;
selectedId: string | null;
}): { enabled: boolean; reason?: string; selected?: boolean } {
return resolveMemorySlotDecisionShared(params);
}