import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; import { CONFIG_PATH_CLAWDBOT, loadConfig, parseConfigJson5, readConfigFileSnapshot, resolveConfigSnapshotHash, validateConfigObject, writeConfigFile, } from "../config/config.js"; import { applyLegacyMigrations } from "../config/legacy.js"; import { applyMergePatch } from "../config/merge-patch.js"; import { buildConfigSchema } from "../config/schema.js"; import { listChannelPlugins } from "../channels/plugins/index.js"; import { loadClawdbotPlugins } from "../plugins/loader.js"; import { ErrorCodes, formatValidationErrors, validateConfigGetParams, validateConfigPatchParams, validateConfigSchemaParams, validateConfigSetParams, } from "./protocol/index.js"; import type { BridgeMethodHandler } from "./server-bridge-types.js"; function resolveBaseHash(params: unknown): string | null { const raw = (params as { baseHash?: unknown })?.baseHash; if (typeof raw !== "string") return null; const trimmed = raw.trim(); return trimmed ? trimmed : null; } function requireConfigBaseHash( params: unknown, snapshot: Awaited>, ): { ok: true } | { ok: false; error: { code: string; message: string } } { if (!snapshot.exists) return { ok: true }; const snapshotHash = resolveConfigSnapshotHash(snapshot); if (!snapshotHash) { return { ok: false, error: { code: ErrorCodes.INVALID_REQUEST, message: "config base hash unavailable; re-run config.get and retry", }, }; } const baseHash = resolveBaseHash(params); if (!baseHash) { return { ok: false, error: { code: ErrorCodes.INVALID_REQUEST, message: "config base hash required; re-run config.get and retry", }, }; } if (baseHash !== snapshotHash) { return { ok: false, error: { code: ErrorCodes.INVALID_REQUEST, message: "config changed since last load; re-run config.get and retry", }, }; } return { ok: true }; } export const handleConfigBridgeMethods: BridgeMethodHandler = async ( _ctx, _nodeId, method, params, ) => { switch (method) { case "config.get": { if (!validateConfigGetParams(params)) { return { ok: false, error: { code: ErrorCodes.INVALID_REQUEST, message: `invalid config.get params: ${formatValidationErrors(validateConfigGetParams.errors)}`, }, }; } const snapshot = await readConfigFileSnapshot(); return { ok: true, payloadJSON: JSON.stringify(snapshot) }; } case "config.schema": { if (!validateConfigSchemaParams(params)) { return { ok: false, error: { code: ErrorCodes.INVALID_REQUEST, message: `invalid config.schema params: ${formatValidationErrors(validateConfigSchemaParams.errors)}`, }, }; } const cfg = loadConfig(); const workspaceDir = resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg)); const pluginRegistry = loadClawdbotPlugins({ config: cfg, workspaceDir, logger: { info: () => {}, warn: () => {}, error: () => {}, debug: () => {}, }, }); const schema = buildConfigSchema({ plugins: pluginRegistry.plugins.map((plugin) => ({ id: plugin.id, name: plugin.name, description: plugin.description, configUiHints: plugin.configUiHints, configSchema: plugin.configJsonSchema, })), channels: listChannelPlugins().map((entry) => ({ id: entry.id, label: entry.meta.label, description: entry.meta.blurb, configSchema: entry.configSchema?.schema, configUiHints: entry.configSchema?.uiHints, })), }); return { ok: true, payloadJSON: JSON.stringify(schema) }; } case "config.set": { if (!validateConfigSetParams(params)) { return { ok: false, error: { code: ErrorCodes.INVALID_REQUEST, message: `invalid config.set params: ${formatValidationErrors(validateConfigSetParams.errors)}`, }, }; } const snapshot = await readConfigFileSnapshot(); const guard = requireConfigBaseHash(params, snapshot); if (!guard.ok) { return { ok: false, error: guard.error }; } const rawValue = (params as { raw?: unknown }).raw; if (typeof rawValue !== "string") { return { ok: false, error: { code: ErrorCodes.INVALID_REQUEST, message: "invalid config.set params: raw (string) required", }, }; } const parsedRes = parseConfigJson5(rawValue); if (!parsedRes.ok) { return { ok: false, error: { code: ErrorCodes.INVALID_REQUEST, message: parsedRes.error, }, }; } const validated = validateConfigObject(parsedRes.parsed); if (!validated.ok) { return { ok: false, error: { code: ErrorCodes.INVALID_REQUEST, message: "invalid config", details: { issues: validated.issues }, }, }; } await writeConfigFile(validated.config); return { ok: true, payloadJSON: JSON.stringify({ ok: true, path: CONFIG_PATH_CLAWDBOT, config: validated.config, }), }; } case "config.patch": { if (!validateConfigPatchParams(params)) { return { ok: false, error: { code: ErrorCodes.INVALID_REQUEST, message: `invalid config.patch params: ${formatValidationErrors(validateConfigPatchParams.errors)}`, }, }; } const snapshot = await readConfigFileSnapshot(); const guard = requireConfigBaseHash(params, snapshot); if (!guard.ok) { return { ok: false, error: guard.error }; } if (!snapshot.valid) { return { ok: false, error: { code: ErrorCodes.INVALID_REQUEST, message: "invalid config; fix before patching", }, }; } const rawValue = (params as { raw?: unknown }).raw; if (typeof rawValue !== "string") { return { ok: false, error: { code: ErrorCodes.INVALID_REQUEST, message: "invalid config.patch params: raw (string) required", }, }; } const parsedRes = parseConfigJson5(rawValue); if (!parsedRes.ok) { return { ok: false, error: { code: ErrorCodes.INVALID_REQUEST, message: parsedRes.error, }, }; } if ( !parsedRes.parsed || typeof parsedRes.parsed !== "object" || Array.isArray(parsedRes.parsed) ) { return { ok: false, error: { code: ErrorCodes.INVALID_REQUEST, message: "config.patch raw must be an object", }, }; } const merged = applyMergePatch(snapshot.config, parsedRes.parsed); const migrated = applyLegacyMigrations(merged); const resolved = migrated.next ?? merged; const validated = validateConfigObject(resolved); if (!validated.ok) { return { ok: false, error: { code: ErrorCodes.INVALID_REQUEST, message: "invalid config", details: { issues: validated.issues }, }, }; } await writeConfigFile(validated.config); return { ok: true, payloadJSON: JSON.stringify({ ok: true, path: CONFIG_PATH_CLAWDBOT, config: validated.config, }), }; } default: return null; } };