Files
clawdbot/src/config/schema.ts
2026-01-13 06:30:20 +00:00

474 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { VERSION } from "../version.js";
import { ClawdbotSchema } from "./zod-schema.js";
export type ConfigUiHint = {
label?: string;
help?: string;
group?: string;
order?: number;
advanced?: boolean;
sensitive?: boolean;
placeholder?: string;
itemTemplate?: unknown;
};
export type ConfigUiHints = Record<string, ConfigUiHint>;
export type ConfigSchema = ReturnType<typeof ClawdbotSchema.toJSONSchema>;
export type ConfigSchemaResponse = {
schema: ConfigSchema;
uiHints: ConfigUiHints;
version: string;
generatedAt: string;
};
export type PluginUiMetadata = {
id: string;
name?: string;
description?: string;
configUiHints?: Record<
string,
Pick<
ConfigUiHint,
"label" | "help" | "advanced" | "sensitive" | "placeholder"
>
>;
};
const GROUP_LABELS: Record<string, string> = {
wizard: "Wizard",
logging: "Logging",
gateway: "Gateway",
agents: "Agents",
tools: "Tools",
bindings: "Bindings",
audio: "Audio",
models: "Models",
messages: "Messages",
commands: "Commands",
session: "Session",
cron: "Cron",
hooks: "Hooks",
ui: "UI",
browser: "Browser",
talk: "Talk",
telegram: "Telegram",
discord: "Discord",
slack: "Slack",
signal: "Signal",
imessage: "iMessage",
whatsapp: "WhatsApp",
skills: "Skills",
plugins: "Plugins",
discovery: "Discovery",
presence: "Presence",
voicewake: "Voice Wake",
};
const GROUP_ORDER: Record<string, number> = {
wizard: 20,
gateway: 30,
agents: 40,
tools: 50,
bindings: 55,
audio: 60,
models: 70,
messages: 80,
commands: 85,
session: 90,
cron: 100,
hooks: 110,
ui: 120,
browser: 130,
talk: 140,
telegram: 150,
discord: 160,
slack: 165,
signal: 170,
imessage: 180,
whatsapp: 190,
skills: 200,
plugins: 205,
discovery: 210,
presence: 220,
voicewake: 230,
logging: 900,
};
const FIELD_LABELS: Record<string, string> = {
"gateway.remote.url": "Remote Gateway URL",
"gateway.remote.sshTarget": "Remote Gateway SSH Target",
"gateway.remote.sshIdentity": "Remote Gateway SSH Identity",
"gateway.remote.token": "Remote Gateway Token",
"gateway.remote.password": "Remote Gateway Password",
"gateway.auth.token": "Gateway Token",
"gateway.auth.password": "Gateway Password",
"tools.audio.transcription.args": "Audio Transcription Args",
"tools.audio.transcription.timeoutSeconds":
"Audio Transcription Timeout (sec)",
"tools.profile": "Tool Profile",
"agents.list[].tools.profile": "Agent Tool Profile",
"tools.exec.applyPatch.enabled": "Enable apply_patch",
"tools.exec.applyPatch.allowModels": "apply_patch Model Allowlist",
"gateway.controlUi.basePath": "Control UI Base Path",
"gateway.http.endpoints.chatCompletions.enabled":
"OpenAI Chat Completions Endpoint",
"gateway.reload.mode": "Config Reload Mode",
"gateway.reload.debounceMs": "Config Reload Debounce (ms)",
"agents.defaults.workspace": "Workspace",
"agents.defaults.bootstrapMaxChars": "Bootstrap Max Chars",
"agents.defaults.memorySearch": "Memory Search",
"agents.defaults.memorySearch.enabled": "Enable Memory Search",
"agents.defaults.memorySearch.provider": "Memory Search Provider",
"agents.defaults.memorySearch.remote.baseUrl": "Remote Embedding Base URL",
"agents.defaults.memorySearch.remote.apiKey": "Remote Embedding API Key",
"agents.defaults.memorySearch.remote.headers": "Remote Embedding Headers",
"agents.defaults.memorySearch.model": "Memory Search Model",
"agents.defaults.memorySearch.fallback": "Memory Search Fallback",
"agents.defaults.memorySearch.local.modelPath": "Local Embedding Model Path",
"agents.defaults.memorySearch.store.path": "Memory Search Index Path",
"agents.defaults.memorySearch.chunking.tokens": "Memory Chunk Tokens",
"agents.defaults.memorySearch.chunking.overlap":
"Memory Chunk Overlap Tokens",
"agents.defaults.memorySearch.sync.onSessionStart": "Index on Session Start",
"agents.defaults.memorySearch.sync.onSearch": "Index on Search (Lazy)",
"agents.defaults.memorySearch.sync.watch": "Watch Memory Files",
"agents.defaults.memorySearch.sync.watchDebounceMs":
"Memory Watch Debounce (ms)",
"agents.defaults.memorySearch.query.maxResults": "Memory Search Max Results",
"agents.defaults.memorySearch.query.minScore": "Memory Search Min Score",
"auth.profiles": "Auth Profiles",
"auth.order": "Auth Profile Order",
"auth.cooldowns.billingBackoffHours": "Billing Backoff (hours)",
"auth.cooldowns.billingBackoffHoursByProvider": "Billing Backoff Overrides",
"auth.cooldowns.billingMaxHours": "Billing Backoff Cap (hours)",
"auth.cooldowns.failureWindowHours": "Failover Window (hours)",
"agents.defaults.models": "Models",
"agents.defaults.model.primary": "Primary Model",
"agents.defaults.model.fallbacks": "Model Fallbacks",
"agents.defaults.imageModel.primary": "Image Model",
"agents.defaults.imageModel.fallbacks": "Image Model Fallbacks",
"agents.defaults.humanDelay.mode": "Human Delay Mode",
"agents.defaults.humanDelay.minMs": "Human Delay Min (ms)",
"agents.defaults.humanDelay.maxMs": "Human Delay Max (ms)",
"agents.defaults.cliBackends": "CLI Backends",
"commands.native": "Native Commands",
"commands.text": "Text Commands",
"commands.config": "Allow /config",
"commands.debug": "Allow /debug",
"commands.restart": "Allow Restart",
"commands.useAccessGroups": "Use Access Groups",
"ui.seamColor": "Accent Color",
"browser.controlUrl": "Browser Control URL",
"session.agentToAgent.maxPingPongTurns": "Agent-to-Agent Ping-Pong Turns",
"messages.ackReaction": "Ack Reaction Emoji",
"messages.ackReactionScope": "Ack Reaction Scope",
"talk.apiKey": "Talk API Key",
"telegram.botToken": "Telegram Bot Token",
"telegram.dmPolicy": "Telegram DM Policy",
"telegram.streamMode": "Telegram Draft Stream Mode",
"telegram.draftChunk.minChars": "Telegram Draft Chunk Min Chars",
"telegram.draftChunk.maxChars": "Telegram Draft Chunk Max Chars",
"telegram.draftChunk.breakPreference":
"Telegram Draft Chunk Break Preference",
"telegram.retry.attempts": "Telegram Retry Attempts",
"telegram.retry.minDelayMs": "Telegram Retry Min Delay (ms)",
"telegram.retry.maxDelayMs": "Telegram Retry Max Delay (ms)",
"telegram.retry.jitter": "Telegram Retry Jitter",
"whatsapp.dmPolicy": "WhatsApp DM Policy",
"whatsapp.selfChatMode": "WhatsApp Self-Phone Mode",
"signal.dmPolicy": "Signal DM Policy",
"imessage.dmPolicy": "iMessage DM Policy",
"discord.dm.policy": "Discord DM Policy",
"discord.retry.attempts": "Discord Retry Attempts",
"discord.retry.minDelayMs": "Discord Retry Min Delay (ms)",
"discord.retry.maxDelayMs": "Discord Retry Max Delay (ms)",
"discord.retry.jitter": "Discord Retry Jitter",
"discord.maxLinesPerMessage": "Discord Max Lines Per Message",
"slack.dm.policy": "Slack DM Policy",
"slack.allowBots": "Slack Allow Bot Messages",
"discord.token": "Discord Bot Token",
"slack.botToken": "Slack Bot Token",
"slack.appToken": "Slack App Token",
"signal.account": "Signal Account",
"imessage.cliPath": "iMessage CLI Path",
"plugins.enabled": "Enable Plugins",
"plugins.allow": "Plugin Allowlist",
"plugins.deny": "Plugin Denylist",
"plugins.load.paths": "Plugin Load Paths",
"plugins.entries": "Plugin Entries",
"plugins.entries.*.enabled": "Plugin Enabled",
"plugins.entries.*.config": "Plugin Config",
};
const FIELD_HELP: Record<string, string> = {
"gateway.remote.url": "Remote Gateway WebSocket URL (ws:// or wss://).",
"gateway.remote.sshTarget":
"Remote gateway over SSH (tunnels the gateway port to localhost). Format: user@host or user@host:port.",
"gateway.remote.sshIdentity":
"Optional SSH identity file path (passed to ssh -i).",
"gateway.auth.token":
"Recommended for all gateways; required for non-loopback binds.",
"gateway.auth.password": "Required for Tailscale funnel.",
"gateway.controlUi.basePath":
"Optional URL prefix where the Control UI is served (e.g. /clawdbot).",
"gateway.http.endpoints.chatCompletions.enabled":
"Enable the OpenAI-compatible `POST /v1/chat/completions` endpoint (default: false).",
"gateway.reload.mode":
'Hot reload strategy for config changes ("hybrid" recommended).',
"gateway.reload.debounceMs":
"Debounce window (ms) before applying config changes.",
"tools.exec.applyPatch.enabled":
"Experimental. Enables apply_patch for OpenAI models when allowed by tool policy.",
"tools.exec.applyPatch.allowModels":
'Optional allowlist of model ids (e.g. "gpt-5.2" or "openai/gpt-5.2").',
"slack.allowBots":
"Allow bot-authored messages to trigger Slack replies (default: false).",
"auth.profiles": "Named auth profiles (provider + mode + optional email).",
"auth.order":
"Ordered auth profile IDs per provider (used for automatic failover).",
"auth.cooldowns.billingBackoffHours":
"Base backoff (hours) when a profile fails due to billing/insufficient credits (default: 5).",
"auth.cooldowns.billingBackoffHoursByProvider":
"Optional per-provider overrides for billing backoff (hours).",
"auth.cooldowns.billingMaxHours":
"Cap (hours) for billing backoff (default: 24).",
"auth.cooldowns.failureWindowHours":
"Failure window (hours) for backoff counters (default: 24).",
"agents.defaults.bootstrapMaxChars":
"Max characters of each workspace bootstrap file injected into the system prompt before truncation (default: 20000).",
"agents.defaults.models":
"Configured model catalog (keys are full provider/model IDs).",
"agents.defaults.memorySearch":
"Vector search over MEMORY.md and memory/*.md (per-agent overrides supported).",
"agents.defaults.memorySearch.provider":
'Embedding provider ("openai" or "local").',
"agents.defaults.memorySearch.remote.baseUrl":
"Custom OpenAI-compatible base URL (e.g. for Gemini/OpenRouter proxies).",
"agents.defaults.memorySearch.remote.apiKey":
"Custom API key for the remote embedding provider.",
"agents.defaults.memorySearch.remote.headers":
"Extra headers for remote embeddings (merged; remote overrides OpenAI headers).",
"agents.defaults.memorySearch.local.modelPath":
"Local GGUF model path or hf: URI (node-llama-cpp).",
"agents.defaults.memorySearch.fallback":
'Fallback to OpenAI when local embeddings fail ("openai" or "none").',
"agents.defaults.memorySearch.store.path":
"SQLite index path (default: ~/.clawdbot/memory/{agentId}.sqlite).",
"agents.defaults.memorySearch.sync.onSearch":
"Lazy sync: reindex on first search after a change.",
"agents.defaults.memorySearch.sync.watch":
"Watch memory files for changes (chokidar).",
"plugins.enabled": "Enable plugin/extension loading (default: true).",
"plugins.allow":
"Optional allowlist of plugin ids; when set, only listed plugins load.",
"plugins.deny": "Optional denylist of plugin ids; deny wins over allowlist.",
"plugins.load.paths": "Additional plugin files or directories to load.",
"plugins.entries":
"Per-plugin settings keyed by plugin id (enable/disable + config payloads).",
"plugins.entries.*.enabled":
"Overrides plugin enable/disable for this entry (restart required).",
"plugins.entries.*.config":
"Plugin-defined config payload (schema is provided by the plugin).",
"agents.defaults.model.primary": "Primary model (provider/model).",
"agents.defaults.model.fallbacks":
"Ordered fallback models (provider/model). Used when the primary model fails.",
"agents.defaults.imageModel.primary":
"Optional image model (provider/model) used when the primary model lacks image input.",
"agents.defaults.imageModel.fallbacks":
"Ordered fallback image models (provider/model).",
"agents.defaults.cliBackends":
"Optional CLI backends for text-only fallback (claude-cli, etc.).",
"agents.defaults.humanDelay.mode":
'Delay style for block replies ("off", "natural", "custom").',
"agents.defaults.humanDelay.minMs":
"Minimum delay in ms for custom humanDelay (default: 800).",
"agents.defaults.humanDelay.maxMs":
"Maximum delay in ms for custom humanDelay (default: 2500).",
"commands.native":
"Register native commands with connectors that support it (Discord/Slack/Telegram).",
"commands.text": "Allow text command parsing (slash commands only).",
"commands.config":
"Allow /config chat command to read/write config on disk (default: false).",
"commands.debug":
"Allow /debug chat command for runtime-only overrides (default: false).",
"commands.restart":
"Allow /restart and gateway restart tool actions (default: false).",
"commands.useAccessGroups":
"Enforce access-group allowlists/policies for commands.",
"discord.commands.native":
'Override native commands for Discord (bool or "auto").',
"telegram.commands.native":
'Override native commands for Telegram (bool or "auto").',
"slack.commands.native":
'Override native commands for Slack (bool or "auto").',
"session.agentToAgent.maxPingPongTurns":
"Max reply-back turns between requester and target (05).",
"messages.ackReaction":
"Emoji reaction used to acknowledge inbound messages (empty disables).",
"messages.ackReactionScope":
'When to send ack reactions ("group-mentions", "group-all", "direct", "all").',
"telegram.dmPolicy":
'Direct message access control ("pairing" recommended). "open" requires telegram.allowFrom=["*"].',
"telegram.streamMode":
"Draft streaming mode for Telegram replies (off | partial | block). Separate from block streaming; requires private topics + sendMessageDraft.",
"telegram.draftChunk.minChars":
'Minimum chars before emitting a Telegram draft update when telegram.streamMode="block" (default: 200).',
"telegram.draftChunk.maxChars":
'Target max size for a Telegram draft update chunk when telegram.streamMode="block" (default: 800; clamped to telegram.textChunkLimit).',
"telegram.draftChunk.breakPreference":
"Preferred breakpoints for Telegram draft chunks (paragraph | newline | sentence). Default: paragraph.",
"telegram.retry.attempts":
"Max retry attempts for outbound Telegram API calls (default: 3).",
"telegram.retry.minDelayMs":
"Minimum retry delay in ms for Telegram outbound calls.",
"telegram.retry.maxDelayMs":
"Maximum retry delay cap in ms for Telegram outbound calls.",
"telegram.retry.jitter":
"Jitter factor (0-1) applied to Telegram retry delays.",
"whatsapp.dmPolicy":
'Direct message access control ("pairing" recommended). "open" requires whatsapp.allowFrom=["*"].',
"whatsapp.selfChatMode":
"Same-phone setup (bot uses your personal WhatsApp number).",
"signal.dmPolicy":
'Direct message access control ("pairing" recommended). "open" requires signal.allowFrom=["*"].',
"imessage.dmPolicy":
'Direct message access control ("pairing" recommended). "open" requires imessage.allowFrom=["*"].',
"discord.dm.policy":
'Direct message access control ("pairing" recommended). "open" requires discord.dm.allowFrom=["*"].',
"discord.retry.attempts":
"Max retry attempts for outbound Discord API calls (default: 3).",
"discord.retry.minDelayMs":
"Minimum retry delay in ms for Discord outbound calls.",
"discord.retry.maxDelayMs":
"Maximum retry delay cap in ms for Discord outbound calls.",
"discord.retry.jitter":
"Jitter factor (0-1) applied to Discord retry delays.",
"discord.maxLinesPerMessage":
"Soft max line count per Discord message (default: 17).",
"slack.dm.policy":
'Direct message access control ("pairing" recommended). "open" requires slack.dm.allowFrom=["*"].',
};
const FIELD_PLACEHOLDERS: Record<string, string> = {
"gateway.remote.url": "ws://host:18789",
"gateway.remote.sshTarget": "user@host",
"gateway.controlUi.basePath": "/clawdbot",
};
const SENSITIVE_PATTERNS = [/token/i, /password/i, /secret/i, /api.?key/i];
function isSensitivePath(path: string): boolean {
return SENSITIVE_PATTERNS.some((pattern) => pattern.test(path));
}
function buildBaseHints(): ConfigUiHints {
const hints: ConfigUiHints = {};
for (const [group, label] of Object.entries(GROUP_LABELS)) {
hints[group] = {
label,
group: label,
order: GROUP_ORDER[group],
};
}
for (const [path, label] of Object.entries(FIELD_LABELS)) {
const current = hints[path];
hints[path] = current ? { ...current, label } : { label };
}
for (const [path, help] of Object.entries(FIELD_HELP)) {
const current = hints[path];
hints[path] = current ? { ...current, help } : { help };
}
for (const [path, placeholder] of Object.entries(FIELD_PLACEHOLDERS)) {
const current = hints[path];
hints[path] = current ? { ...current, placeholder } : { placeholder };
}
return hints;
}
function applySensitiveHints(hints: ConfigUiHints): ConfigUiHints {
const next = { ...hints };
for (const key of Object.keys(next)) {
if (isSensitivePath(key)) {
next[key] = { ...next[key], sensitive: true };
}
}
return next;
}
function applyPluginHints(
hints: ConfigUiHints,
plugins: PluginUiMetadata[],
): ConfigUiHints {
const next: ConfigUiHints = { ...hints };
for (const plugin of plugins) {
const id = plugin.id.trim();
if (!id) continue;
const name = (plugin.name ?? id).trim() || id;
const basePath = `plugins.entries.${id}`;
next[basePath] = {
...next[basePath],
label: name,
help: plugin.description
? `${plugin.description} (plugin: ${id})`
: `Plugin entry for ${id}.`,
};
next[`${basePath}.enabled`] = {
...next[`${basePath}.enabled`],
label: `Enable ${name}`,
};
next[`${basePath}.config`] = {
...next[`${basePath}.config`],
label: `${name} Config`,
help: `Plugin-defined config payload for ${id}.`,
};
const uiHints = plugin.configUiHints ?? {};
for (const [relPathRaw, hint] of Object.entries(uiHints)) {
const relPath = relPathRaw.trim().replace(/^\./, "");
if (!relPath) continue;
const key = `${basePath}.config.${relPath}`;
next[key] = {
...next[key],
...hint,
};
}
}
return next;
}
let cachedBase: ConfigSchemaResponse | null = null;
function buildBaseConfigSchema(): ConfigSchemaResponse {
if (cachedBase) return cachedBase;
const schema = ClawdbotSchema.toJSONSchema({
target: "draft-07",
unrepresentable: "any",
});
schema.title = "ClawdbotConfig";
const hints = applySensitiveHints(buildBaseHints());
const next = {
schema,
uiHints: hints,
version: VERSION,
generatedAt: new Date().toISOString(),
};
cachedBase = next;
return next;
}
export function buildConfigSchema(params?: {
plugins?: PluginUiMetadata[];
}): ConfigSchemaResponse {
const base = buildBaseConfigSchema();
const plugins = params?.plugins ?? [];
if (plugins.length === 0) return base;
const merged = applySensitiveHints(applyPluginHints(base.uiHints, plugins));
return {
...base,
uiHints: merged,
};
}