import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; import { CHANNEL_IDS } from "../channels/registry.js"; import { normalizePluginsConfig, resolveEnableState, resolveMemorySlotDecision, } from "../plugins/config-state.js"; import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js"; import { validateJsonSchemaValue } from "../plugins/schema-validator.js"; import { findDuplicateAgentDirs, formatDuplicateAgentDirError } from "./agent-dirs.js"; import { applyAgentDefaults, applyModelDefaults, applySessionDefaults } from "./defaults.js"; import { findLegacyConfigIssues } from "./legacy.js"; import type { ClawdbotConfig, ConfigValidationIssue } from "./types.js"; import { ClawdbotSchema } from "./zod-schema.js"; export function validateConfigObject( raw: unknown, ): { ok: true; config: ClawdbotConfig } | { ok: false; issues: ConfigValidationIssue[] } { const legacyIssues = findLegacyConfigIssues(raw); if (legacyIssues.length > 0) { return { ok: false, issues: legacyIssues.map((iss) => ({ path: iss.path, message: iss.message, })), }; } const validated = ClawdbotSchema.safeParse(raw); if (!validated.success) { return { ok: false, issues: validated.error.issues.map((iss) => ({ path: iss.path.join("."), message: iss.message, })), }; } const duplicates = findDuplicateAgentDirs(validated.data as ClawdbotConfig); if (duplicates.length > 0) { return { ok: false, issues: [ { path: "agents.list", message: formatDuplicateAgentDirError(duplicates), }, ], }; } return { ok: true, config: applyModelDefaults( applyAgentDefaults(applySessionDefaults(validated.data as ClawdbotConfig)), ), }; } function isRecord(value: unknown): value is Record { return Boolean(value && typeof value === "object" && !Array.isArray(value)); } export function validateConfigObjectWithPlugins(raw: unknown): | { ok: true; config: ClawdbotConfig; warnings: ConfigValidationIssue[]; } | { ok: false; issues: ConfigValidationIssue[]; warnings: ConfigValidationIssue[]; } { const base = validateConfigObject(raw); if (!base.ok) { return { ok: false, issues: base.issues, warnings: [] }; } const config = base.config; const issues: ConfigValidationIssue[] = []; const warnings: ConfigValidationIssue[] = []; const pluginsConfig = config.plugins; const normalizedPlugins = normalizePluginsConfig(pluginsConfig); const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)); const registry = loadPluginManifestRegistry({ config, workspaceDir: workspaceDir ?? undefined, }); const knownIds = new Set(registry.plugins.map((record) => record.id)); for (const diag of registry.diagnostics) { let path = diag.pluginId ? `plugins.entries.${diag.pluginId}` : "plugins"; if (!diag.pluginId && diag.message.includes("plugin path not found")) { path = "plugins.load.paths"; } const pluginLabel = diag.pluginId ? `plugin ${diag.pluginId}` : "plugin"; const message = `${pluginLabel}: ${diag.message}`; if (diag.level === "error") { issues.push({ path, message }); } else { warnings.push({ path, message }); } } const entries = pluginsConfig?.entries; if (entries && isRecord(entries)) { for (const pluginId of Object.keys(entries)) { if (!knownIds.has(pluginId)) { issues.push({ path: `plugins.entries.${pluginId}`, message: `plugin not found: ${pluginId}`, }); } } } const allow = pluginsConfig?.allow ?? []; for (const pluginId of allow) { if (typeof pluginId !== "string" || !pluginId.trim()) continue; if (!knownIds.has(pluginId)) { issues.push({ path: "plugins.allow", message: `plugin not found: ${pluginId}`, }); } } const deny = pluginsConfig?.deny ?? []; for (const pluginId of deny) { if (typeof pluginId !== "string" || !pluginId.trim()) continue; if (!knownIds.has(pluginId)) { issues.push({ path: "plugins.deny", message: `plugin not found: ${pluginId}`, }); } } const memorySlot = normalizedPlugins.slots.memory; if (typeof memorySlot === "string" && memorySlot.trim() && !knownIds.has(memorySlot)) { issues.push({ path: "plugins.slots.memory", message: `plugin not found: ${memorySlot}`, }); } const allowedChannels = new Set(["defaults", ...CHANNEL_IDS]); for (const record of registry.plugins) { for (const channelId of record.channels) { allowedChannels.add(channelId); } } if (config.channels && isRecord(config.channels)) { for (const key of Object.keys(config.channels)) { const trimmed = key.trim(); if (!trimmed) continue; if (!allowedChannels.has(trimmed)) { issues.push({ path: `channels.${trimmed}`, message: `unknown channel id: ${trimmed}`, }); } } } let selectedMemoryPluginId: string | null = null; const seenPlugins = new Set(); for (const record of registry.plugins) { const pluginId = record.id; if (seenPlugins.has(pluginId)) { continue; } seenPlugins.add(pluginId); const entry = normalizedPlugins.entries[pluginId]; const entryHasConfig = Boolean(entry?.config); const enableState = resolveEnableState(pluginId, record.origin, normalizedPlugins); let enabled = enableState.enabled; let reason = enableState.reason; if (enabled) { const memoryDecision = resolveMemorySlotDecision({ id: pluginId, kind: record.kind, slot: memorySlot, selectedId: selectedMemoryPluginId, }); if (!memoryDecision.enabled) { enabled = false; reason = memoryDecision.reason; } if (memoryDecision.selected && record.kind === "memory") { selectedMemoryPluginId = pluginId; } } const shouldValidate = enabled || entryHasConfig; if (shouldValidate) { if (record.configSchema) { const res = validateJsonSchemaValue({ schema: record.configSchema, cacheKey: record.schemaCacheKey ?? record.manifestPath ?? pluginId, value: entry?.config ?? {}, }); if (!res.ok) { for (const error of res.errors) { issues.push({ path: `plugins.entries.${pluginId}.config`, message: `invalid config: ${error}`, }); } } } else { issues.push({ path: `plugins.entries.${pluginId}`, message: `plugin schema missing for ${pluginId}`, }); } } if (!enabled && entryHasConfig) { warnings.push({ path: `plugins.entries.${pluginId}`, message: `plugin disabled (${reason ?? "disabled"}) but config is present`, }); } } if (issues.length > 0) { return { ok: false, issues, warnings }; } return { ok: true, config, warnings }; }