Files
clawdbot/src/gateway/server-bridge-methods-config.ts
2026-01-17 00:43:05 +00:00

271 lines
7.8 KiB
TypeScript

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<ReturnType<typeof readConfigFileSnapshot>>,
): { 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;
}
};