import { z } from 'zod'; const telegramSchema = z.object({ bot_token: z.string().min(1, 'Bot token is required'), allowed_chat_ids: z.array(z.number()).min(1, 'At least one chat ID required'), require_mention: z.boolean().default(true), }); const serverSchema = z.object({ tailscale_only: z.boolean().default(true), localhost: z.boolean().default(true), port: z.number().default(18800), /** Static bearer token for gateway auth. If set, all connections must provide it. */ token: z.string().optional(), /** Trust Tailscale-User-Login header for identity. */ tailscale_identity: z.boolean().default(false), /** Apply token auth to HTTP requests too (not just WebSocket). Default: true when token is set. */ auth_http: z.boolean().default(true), }); const modelConfigSchema = z.object({ provider: z.enum(['anthropic', 'openai', 'gemini', 'ollama', 'llamacpp', 'openrouter', 'bedrock', 'github']), model: z.string(), endpoint: z.string().optional(), api_key: z.string().optional(), auth_token: z.string().optional(), for: z.array(z.string()).optional(), num_gpu: z.number().optional(), context_window: z.number().optional(), }); const modelsSchema = z.object({ local: modelConfigSchema.optional(), fast: modelConfigSchema.optional(), default: modelConfigSchema, complex: modelConfigSchema.optional(), fallback_chain: z.array(z.string()).default(['anthropic']), local_providers: z.record(z.string(), modelConfigSchema).optional(), }); const backendsSchema = z.object({ claude_code: z.object({ enabled: z.boolean().default(false), path: z.string().optional(), }).default({ enabled: false }), opencode: z.object({ enabled: z.boolean().default(false), path: z.string().optional(), }).default({ enabled: false }), native: z.object({ enabled: z.boolean().default(true), }).default({ enabled: true }), }).default({}); const hooksSchema = z.object({ confirm: z.array(z.string()).default([]), log: z.array(z.string()).default([]), silent: z.array(z.string()).default([]), }).default({}); const skillsSchema = z.object({ /** Directory for user-created workspace skills. */ workspace_dir: z.string().optional(), /** Directory for managed (installed) skills. Defaults to ~/.flynn/workspace/skills. */ managed_dir: z.string().optional(), /** Directory for bundled skills shipped with Flynn. */ bundled_dir: z.string().optional(), }).default({}); const mcpServerSchema = z.object({ name: z.string(), command: z.string(), args: z.array(z.string()).default([]), env: z.record(z.string(), z.string()).optional(), cwd: z.string().optional(), }); const mcpSchema = z.object({ servers: z.array(mcpServerSchema).default([]), }).default({ servers: [] }); const cronJobSchema = z.object({ name: z.string().min(1, 'Cron job name is required'), schedule: z.string().min(1, 'Cron schedule is required'), message: z.string().min(1, 'Cron message is required'), output: z.object({ channel: z.string().min(1), peer: z.string().min(1), }), enabled: z.boolean().default(true), timezone: z.string().optional(), }); const automationSchema = z.object({ cron: z.array(cronJobSchema).default([]), }).default({}); const agentsSchema = z.object({ primary_tier: z.enum(['fast', 'default', 'complex', 'local']).default('default'), delegation: z.object({ compaction: z.enum(['fast', 'default', 'complex', 'local']).default('fast'), memory_extraction: z.enum(['fast', 'default', 'complex', 'local']).default('fast'), classification: z.enum(['fast', 'default', 'complex', 'local']).default('fast'), tool_summarisation: z.enum(['fast', 'default', 'complex', 'local']).default('fast'), complex_reasoning: z.enum(['fast', 'default', 'complex', 'local']).default('complex'), }).default({ compaction: 'fast', memory_extraction: 'fast', classification: 'fast', tool_summarisation: 'fast', complex_reasoning: 'complex', }), auto_escalate: z.boolean().default(false), max_delegation_depth: z.number().min(1).max(10).default(3), }).default({}); const memorySchema = z.object({ enabled: z.boolean().default(true), dir: z.string().optional(), // Default: ~/.local/share/flynn/memory auto_extract: z.boolean().default(true), max_context_tokens: z.number().min(100).max(10000).default(2000), }).default({}); const compactionSchema = z.object({ enabled: z.boolean().default(true), threshold_pct: z.number().min(10).max(100).default(80), keep_turns: z.number().min(1).max(50).default(4), summary_max_tokens: z.number().min(128).max(4096).default(1024), }).default({}); const discordSchema = z.object({ bot_token: z.string().min(1, 'Bot token is required'), allowed_guild_ids: z.array(z.string()).default([]), allowed_channel_ids: z.array(z.string()).default([]), require_mention: z.boolean().default(true), }).optional(); const slackSchema = z.object({ bot_token: z.string().min(1, 'Bot token is required'), app_token: z.string().min(1, 'App token is required'), signing_secret: z.string().min(1, 'Signing secret is required'), allowed_channel_ids: z.array(z.string()).default([]), require_mention: z.boolean().default(false), }).optional(); const whatsappSchema = z.object({ allowed_numbers: z.array(z.string()).default([]), allowed_group_ids: z.array(z.string()).default([]), require_mention: z.boolean().default(true), data_dir: z.string().optional(), }).optional(); const browserSchema = z.object({ enabled: z.boolean().default(false), executable_path: z.string().optional(), ws_endpoint: z.string().optional(), headless: z.boolean().default(true), max_pages: z.number().min(1).max(20).default(5), default_timeout: z.number().min(1000).max(120000).default(30000), }).default({}); const processSchema = z.object({ max_concurrent: z.number().min(1).max(50).default(10), max_runtime_minutes: z.number().min(1).max(1440).default(60), buffer_size: z.number().min(1024).max(1048576).default(65536), }).default({}); const retrySchema = z.object({ enabled: z.boolean().default(true), max_retries: z.number().min(0).max(10).default(3), initial_delay_ms: z.number().min(100).max(60000).default(1000), backoff_multiplier: z.number().min(1).max(5).default(2), max_delay_ms: z.number().min(1000).max(120000).default(30000), }).default({}); const webSearchSchema = z.object({ provider: z.enum(['brave', 'searxng']).default('brave'), api_key: z.string().optional(), endpoint: z.string().optional(), max_results: z.number().min(1).max(20).default(5), }).default({}); // ── Tool policy schemas ────────────────────────────────────────────── const toolProfileEnum = z.enum(['minimal', 'messaging', 'coding', 'full']); const toolOverrideSchema = z.object({ profile: toolProfileEnum.optional(), allow: z.array(z.string()).default([]), deny: z.array(z.string()).default([]), }).default({}); const toolsSchema = z.object({ profile: toolProfileEnum.default('full'), allow: z.array(z.string()).default([]), deny: z.array(z.string()).default([]), agents: z.record(z.string(), toolOverrideSchema).default({}), providers: z.record(z.string(), toolOverrideSchema).default({}), }).default({}); // ── Sandbox schemas ─────────────────────────────────────────────────── const sandboxSchema = z.object({ enabled: z.boolean().default(false), image: z.string().default('node:22-slim'), workspace_dir: z.string().default('/workspace'), network: z.enum(['none', 'bridge', 'host']).default('none'), memory_limit: z.string().default('512m'), cpu_limit: z.string().default('1.0'), timeout_seconds: z.number().min(10).max(3600).default(300), }).default({}); // ── Agent config + routing schemas ──────────────────────────────────── const modelTierEnum = z.enum(['fast', 'default', 'complex', 'local']); const agentConfigEntrySchema = z.object({ system_prompt: z.string().optional(), model_tier: modelTierEnum.optional(), tool_profile: toolProfileEnum.optional(), tool_overrides: toolOverrideSchema.optional(), sandbox: z.boolean().default(false), }); const agentConfigsSchema = z.record(z.string(), agentConfigEntrySchema).default({}); const routingSchema = z.object({ default_agent: z.string().optional(), channels: z.record(z.string(), z.string()).default({}), senders: z.record(z.string(), z.string()).default({}), }).default({}); const promptSchema = z.object({ /** Additional directories to search for prompt template files. */ search_dirs: z.array(z.string()).default([]), /** Extra named sections to include in the system prompt. */ extra_sections: z.array(z.object({ name: z.string(), content: z.string(), })).default([]), }).default({}); export const configSchema = z.object({ telegram: telegramSchema, discord: discordSchema, slack: slackSchema, whatsapp: whatsappSchema, server: serverSchema.default({}), models: modelsSchema, backends: backendsSchema.default({}), hooks: hooksSchema.default({}), skills: skillsSchema.default({}), mcp: mcpSchema.default({ servers: [] }), automation: automationSchema, agents: agentsSchema, compaction: compactionSchema, memory: memorySchema, process: processSchema, browser: browserSchema, retry: retrySchema, web_search: webSearchSchema, prompt: promptSchema, tools: toolsSchema, sandbox: sandboxSchema, agent_configs: agentConfigsSchema, routing: routingSchema, }); export type Config = z.infer; export type TelegramConfig = z.infer; export type ModelConfig = z.infer; export type CronJobConfig = z.infer; export type AgentsConfig = z.infer; export type CompactionConfig = z.infer; export type MemoryConfig = z.infer; export type WebSearchConfig = z.infer; export type ProcessConfig = z.infer; export type BrowserConfig = z.infer; export type DiscordConfig = z.infer; export type SlackConfig = z.infer; export type WhatsAppConfig = z.infer; export type RetryPolicyConfig = z.infer; export type PromptConfig = z.infer; export type ToolProfile = z.infer; export type ToolOverrideConfig = z.infer; export type ToolsConfig = z.infer; export type SandboxConfig = z.infer; export type AgentConfigEntry = z.infer; export type RoutingConfig = z.infer; export type ServerConfig = z.infer;