import { normalizeChatChannelId } from "../channels/registry.js"; import type { OpenClawConfig } from "../config/config.js"; import type { PluginRecord } from "./registry.js"; import { defaultSlotIdForKey } from "./slots.js"; export type NormalizedPluginsConfig = { enabled: boolean; allow: string[]; deny: string[]; loadPaths: string[]; slots: { memory?: string | null; }; entries: Record< string, { enabled?: boolean; hooks?: { allowPromptInjection?: boolean; }; subagent?: { allowModelOverride?: boolean; allowedModels?: string[]; hasAllowedModelsConfig?: boolean; }; config?: unknown; } >; }; export const BUNDLED_ENABLED_BY_DEFAULT = new Set([ "amazon-bedrock", "anthropic", "byteplus", "cloudflare-ai-gateway", "device-pair", "github-copilot", "google", "huggingface", "kilocode", "kimi", "minimax", "mistral", "modelstudio", "moonshot", "nvidia", "ollama", "openai", "opencode", "opencode-go", "openrouter", "phone-control", "qianfan", "qwen-portal-auth", "sglang", "synthetic", "talk-voice", "together", "venice", "vercel-ai-gateway", "vllm", "volcengine", "xai", "xiaomi", "zai", ]); const PLUGIN_ID_ALIASES: Readonly> = { "openai-codex": "openai", "kimi-coding": "kimi", "minimax-portal-auth": "minimax", }; function normalizePluginId(id: string): string { const trimmed = id.trim(); return PLUGIN_ID_ALIASES[trimmed] ?? trimmed; } const normalizeList = (value: unknown): string[] => { if (!Array.isArray(value)) { return []; } return value .map((entry) => (typeof entry === "string" ? normalizePluginId(entry) : "")) .filter(Boolean); }; const normalizeSlotValue = (value: unknown): string | null | undefined => { if (typeof value !== "string") { return undefined; } const trimmed = value.trim(); if (!trimmed) { return undefined; } if (trimmed.toLowerCase() === "none") { return null; } return trimmed; }; const normalizePluginEntries = (entries: unknown): NormalizedPluginsConfig["entries"] => { if (!entries || typeof entries !== "object" || Array.isArray(entries)) { return {}; } const normalized: NormalizedPluginsConfig["entries"] = {}; for (const [key, value] of Object.entries(entries)) { const normalizedKey = normalizePluginId(key); if (!normalizedKey) { continue; } if (!value || typeof value !== "object" || Array.isArray(value)) { normalized[normalizedKey] = {}; continue; } const entry = value as Record; const hooksRaw = entry.hooks; const hooks = hooksRaw && typeof hooksRaw === "object" && !Array.isArray(hooksRaw) ? { allowPromptInjection: (hooksRaw as { allowPromptInjection?: unknown }) .allowPromptInjection, } : undefined; const normalizedHooks = hooks && typeof hooks.allowPromptInjection === "boolean" ? { allowPromptInjection: hooks.allowPromptInjection, } : undefined; const subagentRaw = entry.subagent; const subagent = subagentRaw && typeof subagentRaw === "object" && !Array.isArray(subagentRaw) ? { allowModelOverride: (subagentRaw as { allowModelOverride?: unknown }) .allowModelOverride, hasAllowedModelsConfig: Array.isArray( (subagentRaw as { allowedModels?: unknown }).allowedModels, ), allowedModels: Array.isArray((subagentRaw as { allowedModels?: unknown }).allowedModels) ? ((subagentRaw as { allowedModels?: unknown }).allowedModels as unknown[]) .map((model) => (typeof model === "string" ? model.trim() : "")) .filter(Boolean) : undefined, } : undefined; const normalizedSubagent = subagent && (typeof subagent.allowModelOverride === "boolean" || subagent.hasAllowedModelsConfig || (Array.isArray(subagent.allowedModels) && subagent.allowedModels.length > 0)) ? { ...(typeof subagent.allowModelOverride === "boolean" ? { allowModelOverride: subagent.allowModelOverride } : {}), ...(subagent.hasAllowedModelsConfig ? { hasAllowedModelsConfig: true } : {}), ...(Array.isArray(subagent.allowedModels) && subagent.allowedModels.length > 0 ? { allowedModels: subagent.allowedModels } : {}), } : undefined; normalized[normalizedKey] = { ...normalized[normalizedKey], enabled: typeof entry.enabled === "boolean" ? entry.enabled : normalized[normalizedKey]?.enabled, hooks: normalizedHooks ?? normalized[normalizedKey]?.hooks, subagent: normalizedSubagent ?? normalized[normalizedKey]?.subagent, config: "config" in entry ? entry.config : normalized[normalizedKey]?.config, }; } return normalized; }; export const normalizePluginsConfig = ( config?: OpenClawConfig["plugins"], ): NormalizedPluginsConfig => { const memorySlot = normalizeSlotValue(config?.slots?.memory); return { enabled: config?.enabled !== false, allow: normalizeList(config?.allow), deny: normalizeList(config?.deny), loadPaths: normalizeList(config?.load?.paths), slots: { memory: memorySlot === undefined ? defaultSlotIdForKey("memory") : memorySlot, }, entries: normalizePluginEntries(config?.entries), }; }; 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")); const hasExplicitPluginConfig = (plugins?: OpenClawConfig["plugins"]) => { if (!plugins) { return false; } if (typeof plugins.enabled === "boolean") { return true; } if (Array.isArray(plugins.allow) && plugins.allow.length > 0) { return true; } if (Array.isArray(plugins.deny) && plugins.deny.length > 0) { return true; } if (plugins.load?.paths && Array.isArray(plugins.load.paths) && plugins.load.paths.length > 0) { return true; } if (plugins.slots && Object.keys(plugins.slots).length > 0) { return true; } if (plugins.entries && Object.keys(plugins.entries).length > 0) { return true; } return false; }; 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; } export function resolveEnableState( id: string, origin: PluginRecord["origin"], config: NormalizedPluginsConfig, enabledByDefault?: boolean, ): { enabled: boolean; reason?: string } { if (!config.enabled) { return { enabled: false, reason: "plugins disabled" }; } if (config.deny.includes(id)) { return { enabled: false, reason: "blocked by denylist" }; } const entry = config.entries[id]; if (entry?.enabled === false) { return { enabled: false, reason: "disabled in config" }; } const explicitlyAllowed = config.allow.includes(id); if (origin === "workspace" && !explicitlyAllowed && entry?.enabled !== true) { return { enabled: false, reason: "workspace plugin (disabled by default)" }; } if (config.slots.memory === id) { return { enabled: true }; } if (config.allow.length > 0 && !explicitlyAllowed) { return { enabled: false, reason: "not in allowlist" }; } if (entry?.enabled === true) { return { enabled: true }; } if (origin === "bundled" && (enabledByDefault ?? BUNDLED_ENABLED_BY_DEFAULT.has(id))) { return { enabled: true }; } if (origin === "bundled") { return { enabled: false, reason: "bundled (disabled by default)" }; } return { enabled: true }; } export function isBundledChannelEnabledByChannelConfig( cfg: OpenClawConfig | undefined, pluginId: string, ): boolean { if (!cfg) { return false; } const channelId = normalizeChatChannelId(pluginId); if (!channelId) { return false; } const channels = cfg.channels as Record | undefined; const entry = channels?.[channelId]; if (!entry || typeof entry !== "object" || Array.isArray(entry)) { return false; } return (entry as Record).enabled === true; } export function resolveEffectiveEnableState(params: { id: string; origin: PluginRecord["origin"]; config: NormalizedPluginsConfig; rootConfig?: OpenClawConfig; enabledByDefault?: boolean; }): { enabled: boolean; reason?: string } { const base = resolveEnableState(params.id, params.origin, params.config, params.enabledByDefault); if ( !base.enabled && base.reason === "bundled (disabled by default)" && isBundledChannelEnabledByChannelConfig(params.rootConfig, params.id) ) { return { enabled: true }; } return base; } export function resolveMemorySlotDecision(params: { id: string; kind?: string; slot: string | null | undefined; selectedId: string | null; }): { enabled: boolean; reason?: string; selected?: boolean } { if (params.kind !== "memory") { return { enabled: true }; } if (params.slot === null) { return { enabled: false, reason: "memory slot disabled" }; } if (typeof params.slot === "string") { if (params.slot === params.id) { return { enabled: true, selected: true }; } return { enabled: false, reason: `memory slot set to "${params.slot}"`, }; } if (params.selectedId && params.selectedId !== params.id) { return { enabled: false, reason: `memory slot already filled by "${params.selectedId}"`, }; } return { enabled: true, selected: true }; }