Files
clawdbot/src/config/zod-schema.ts
2026-01-20 18:56:15 +00:00

483 lines
15 KiB
TypeScript

import { z } from "zod";
import { ToolsSchema } from "./zod-schema.agent-runtime.js";
import { AgentsSchema, AudioSchema, BindingsSchema, BroadcastSchema } from "./zod-schema.agents.js";
import { HexColorSchema, ModelsConfigSchema } from "./zod-schema.core.js";
import { HookMappingSchema, HooksGmailSchema, InternalHooksSchema } from "./zod-schema.hooks.js";
import { ChannelsSchema } from "./zod-schema.providers.js";
import { CommandsSchema, MessagesSchema, SessionSchema } from "./zod-schema.session.js";
export const ClawdbotSchema = z
.object({
meta: z
.object({
lastTouchedVersion: z.string().optional(),
lastTouchedAt: z.string().optional(),
})
.strict()
.optional(),
env: z
.object({
shellEnv: z
.object({
enabled: z.boolean().optional(),
timeoutMs: z.number().int().nonnegative().optional(),
})
.strict()
.optional(),
vars: z.record(z.string(), z.string()).optional(),
})
.catchall(z.string())
.optional(),
wizard: z
.object({
lastRunAt: z.string().optional(),
lastRunVersion: z.string().optional(),
lastRunCommit: z.string().optional(),
lastRunCommand: z.string().optional(),
lastRunMode: z.union([z.literal("local"), z.literal("remote")]).optional(),
})
.strict()
.optional(),
diagnostics: z
.object({
enabled: z.boolean().optional(),
otel: z
.object({
enabled: z.boolean().optional(),
endpoint: z.string().optional(),
protocol: z.union([z.literal("http/protobuf"), z.literal("grpc")]).optional(),
headers: z.record(z.string(), z.string()).optional(),
serviceName: z.string().optional(),
traces: z.boolean().optional(),
metrics: z.boolean().optional(),
logs: z.boolean().optional(),
sampleRate: z.number().min(0).max(1).optional(),
flushIntervalMs: z.number().int().nonnegative().optional(),
})
.strict()
.optional(),
})
.strict()
.optional(),
logging: z
.object({
level: z
.union([
z.literal("silent"),
z.literal("fatal"),
z.literal("error"),
z.literal("warn"),
z.literal("info"),
z.literal("debug"),
z.literal("trace"),
])
.optional(),
file: z.string().optional(),
consoleLevel: z
.union([
z.literal("silent"),
z.literal("fatal"),
z.literal("error"),
z.literal("warn"),
z.literal("info"),
z.literal("debug"),
z.literal("trace"),
])
.optional(),
consoleStyle: z
.union([z.literal("pretty"), z.literal("compact"), z.literal("json")])
.optional(),
redactSensitive: z.union([z.literal("off"), z.literal("tools")]).optional(),
redactPatterns: z.array(z.string()).optional(),
})
.strict()
.optional(),
update: z
.object({
channel: z.union([z.literal("stable"), z.literal("beta"), z.literal("dev")]).optional(),
checkOnStart: z.boolean().optional(),
})
.strict()
.optional(),
browser: z
.object({
enabled: z.boolean().optional(),
controlUrl: z.string().optional(),
controlToken: z.string().optional(),
cdpUrl: z.string().optional(),
remoteCdpTimeoutMs: z.number().int().nonnegative().optional(),
remoteCdpHandshakeTimeoutMs: z.number().int().nonnegative().optional(),
color: z.string().optional(),
executablePath: z.string().optional(),
headless: z.boolean().optional(),
noSandbox: z.boolean().optional(),
attachOnly: z.boolean().optional(),
defaultProfile: z.string().optional(),
profiles: z
.record(
z
.string()
.regex(/^[a-z0-9-]+$/, "Profile names must be alphanumeric with hyphens only"),
z
.object({
cdpPort: z.number().int().min(1).max(65535).optional(),
cdpUrl: z.string().optional(),
driver: z.union([z.literal("clawd"), z.literal("extension")]).optional(),
color: HexColorSchema,
})
.strict()
.refine((value) => value.cdpPort || value.cdpUrl, {
message: "Profile must set cdpPort or cdpUrl",
}),
)
.optional(),
})
.strict()
.optional(),
ui: z
.object({
seamColor: HexColorSchema.optional(),
})
.strict()
.optional(),
auth: z
.object({
profiles: z
.record(
z.string(),
z
.object({
provider: z.string(),
mode: z.union([z.literal("api_key"), z.literal("oauth"), z.literal("token")]),
email: z.string().optional(),
})
.strict(),
)
.optional(),
order: z.record(z.string(), z.array(z.string())).optional(),
cooldowns: z
.object({
billingBackoffHours: z.number().positive().optional(),
billingBackoffHoursByProvider: z.record(z.string(), z.number().positive()).optional(),
billingMaxHours: z.number().positive().optional(),
failureWindowHours: z.number().positive().optional(),
})
.strict()
.optional(),
})
.strict()
.optional(),
models: ModelsConfigSchema,
agents: AgentsSchema,
tools: ToolsSchema,
bindings: BindingsSchema,
broadcast: BroadcastSchema,
audio: AudioSchema,
messages: MessagesSchema,
commands: CommandsSchema,
session: SessionSchema,
cron: z
.object({
enabled: z.boolean().optional(),
store: z.string().optional(),
maxConcurrentRuns: z.number().int().positive().optional(),
})
.strict()
.optional(),
hooks: z
.object({
enabled: z.boolean().optional(),
path: z.string().optional(),
token: z.string().optional(),
maxBodyBytes: z.number().int().positive().optional(),
presets: z.array(z.string()).optional(),
transformsDir: z.string().optional(),
mappings: z.array(HookMappingSchema).optional(),
gmail: HooksGmailSchema,
internal: InternalHooksSchema,
})
.strict()
.optional(),
web: z
.object({
enabled: z.boolean().optional(),
heartbeatSeconds: z.number().int().positive().optional(),
reconnect: z
.object({
initialMs: z.number().positive().optional(),
maxMs: z.number().positive().optional(),
factor: z.number().positive().optional(),
jitter: z.number().min(0).max(1).optional(),
maxAttempts: z.number().int().min(0).optional(),
})
.strict()
.optional(),
})
.strict()
.optional(),
channels: ChannelsSchema,
discovery: z
.object({
wideArea: z
.object({
enabled: z.boolean().optional(),
})
.strict()
.optional(),
})
.strict()
.optional(),
canvasHost: z
.object({
enabled: z.boolean().optional(),
root: z.string().optional(),
port: z.number().int().positive().optional(),
liveReload: z.boolean().optional(),
})
.strict()
.optional(),
talk: z
.object({
voiceId: z.string().optional(),
voiceAliases: z.record(z.string(), z.string()).optional(),
modelId: z.string().optional(),
outputFormat: z.string().optional(),
apiKey: z.string().optional(),
interruptOnSpeech: z.boolean().optional(),
})
.strict()
.optional(),
gateway: z
.object({
port: z.number().int().positive().optional(),
mode: z.union([z.literal("local"), z.literal("remote")]).optional(),
bind: z
.union([z.literal("auto"), z.literal("lan"), z.literal("loopback"), z.literal("custom")])
.optional(),
controlUi: z
.object({
enabled: z.boolean().optional(),
basePath: z.string().optional(),
})
.strict()
.optional(),
auth: z
.object({
mode: z.union([z.literal("token"), z.literal("password")]).optional(),
token: z.string().optional(),
password: z.string().optional(),
allowTailscale: z.boolean().optional(),
})
.strict()
.optional(),
tailscale: z
.object({
mode: z.union([z.literal("off"), z.literal("serve"), z.literal("funnel")]).optional(),
resetOnExit: z.boolean().optional(),
})
.strict()
.optional(),
remote: z
.object({
url: z.string().optional(),
token: z.string().optional(),
password: z.string().optional(),
tlsFingerprint: z.string().optional(),
sshTarget: z.string().optional(),
sshIdentity: z.string().optional(),
})
.strict()
.optional(),
reload: z
.object({
mode: z
.union([
z.literal("off"),
z.literal("restart"),
z.literal("hot"),
z.literal("hybrid"),
])
.optional(),
debounceMs: z.number().int().min(0).optional(),
})
.strict()
.optional(),
tls: z
.object({
enabled: z.boolean().optional(),
autoGenerate: z.boolean().optional(),
certPath: z.string().optional(),
keyPath: z.string().optional(),
caPath: z.string().optional(),
})
.optional(),
http: z
.object({
endpoints: z
.object({
chatCompletions: z
.object({
enabled: z.boolean().optional(),
})
.strict()
.optional(),
responses: z
.object({
enabled: z.boolean().optional(),
maxBodyBytes: z.number().int().positive().optional(),
files: z
.object({
allowUrl: z.boolean().optional(),
allowedMimes: z.array(z.string()).optional(),
maxBytes: z.number().int().positive().optional(),
maxChars: z.number().int().positive().optional(),
maxRedirects: z.number().int().nonnegative().optional(),
timeoutMs: z.number().int().positive().optional(),
pdf: z
.object({
maxPages: z.number().int().positive().optional(),
maxPixels: z.number().int().positive().optional(),
minTextChars: z.number().int().nonnegative().optional(),
})
.strict()
.optional(),
})
.strict()
.optional(),
images: z
.object({
allowUrl: z.boolean().optional(),
allowedMimes: z.array(z.string()).optional(),
maxBytes: z.number().int().positive().optional(),
maxRedirects: z.number().int().nonnegative().optional(),
timeoutMs: z.number().int().positive().optional(),
})
.strict()
.optional(),
})
.strict()
.optional(),
})
.strict()
.optional(),
})
.strict()
.optional(),
nodes: z
.object({
allowCommands: z.array(z.string()).optional(),
denyCommands: z.array(z.string()).optional(),
})
.strict()
.optional(),
})
.strict()
.optional(),
skills: z
.object({
allowBundled: z.array(z.string()).optional(),
load: z
.object({
extraDirs: z.array(z.string()).optional(),
watch: z.boolean().optional(),
watchDebounceMs: z.number().int().min(0).optional(),
})
.strict()
.optional(),
install: z
.object({
preferBrew: z.boolean().optional(),
nodeManager: z
.union([z.literal("npm"), z.literal("pnpm"), z.literal("yarn"), z.literal("bun")])
.optional(),
})
.strict()
.optional(),
entries: z
.record(
z.string(),
z
.object({
enabled: z.boolean().optional(),
apiKey: z.string().optional(),
env: z.record(z.string(), z.string()).optional(),
config: z.record(z.string(), z.unknown()).optional(),
})
.strict(),
)
.optional(),
})
.strict()
.optional(),
plugins: z
.object({
enabled: z.boolean().optional(),
allow: z.array(z.string()).optional(),
deny: z.array(z.string()).optional(),
load: z
.object({
paths: z.array(z.string()).optional(),
})
.strict()
.optional(),
slots: z
.object({
memory: z.string().optional(),
})
.strict()
.optional(),
entries: z
.record(
z.string(),
z
.object({
enabled: z.boolean().optional(),
config: z.record(z.string(), z.unknown()).optional(),
})
.strict(),
)
.optional(),
installs: z
.record(
z.string(),
z
.object({
source: z.union([z.literal("npm"), z.literal("archive"), z.literal("path")]),
spec: z.string().optional(),
sourcePath: z.string().optional(),
installPath: z.string().optional(),
version: z.string().optional(),
installedAt: z.string().optional(),
})
.strict(),
)
.optional(),
})
.strict()
.optional(),
})
.strict()
.superRefine((cfg, ctx) => {
const agents = cfg.agents?.list ?? [];
if (agents.length === 0) return;
const agentIds = new Set(agents.map((agent) => agent.id));
const broadcast = cfg.broadcast;
if (!broadcast) return;
for (const [peerId, ids] of Object.entries(broadcast)) {
if (peerId === "strategy") continue;
if (!Array.isArray(ids)) continue;
for (let idx = 0; idx < ids.length; idx += 1) {
const agentId = ids[idx];
if (!agentIds.has(agentId)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["broadcast", peerId, idx],
message: `Unknown agent id "${agentId}" (not in agents.list).`,
});
}
}
}
});