| 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; |
| }; |
| config?: unknown; |
| } |
| >; |
| }; |
|
|
| export const BUNDLED_ENABLED_BY_DEFAULT = new Set<string>([ |
| "device-pair", |
| "ollama", |
| "phone-control", |
| "sglang", |
| "talk-voice", |
| "vllm", |
| ]); |
|
|
| const normalizeList = (value: unknown): string[] => { |
| if (!Array.isArray(value)) { |
| return []; |
| } |
| return value.map((entry) => (typeof entry === "string" ? entry.trim() : "")).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)) { |
| if (!key.trim()) { |
| continue; |
| } |
| if (!value || typeof value !== "object" || Array.isArray(value)) { |
| normalized[key] = {}; |
| continue; |
| } |
| const entry = value as Record<string, unknown>; |
| 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; |
| normalized[key] = { |
| enabled: typeof entry.enabled === "boolean" ? entry.enabled : undefined, |
| hooks: normalizedHooks, |
| config: "config" in entry ? entry.config : undefined, |
| }; |
| } |
| 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, |
| ): { 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" && 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<string, unknown> | undefined; |
| const entry = channels?.[channelId]; |
| if (!entry || typeof entry !== "object" || Array.isArray(entry)) { |
| return false; |
| } |
| return (entry as Record<string, unknown>).enabled === true; |
| } |
|
|
| export function resolveEffectiveEnableState(params: { |
| id: string; |
| origin: PluginRecord["origin"]; |
| config: NormalizedPluginsConfig; |
| rootConfig?: OpenClawConfig; |
| }): { enabled: boolean; reason?: string } { |
| const base = resolveEnableState(params.id, params.origin, params.config); |
| 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 }; |
| } |
|
|