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 tailscaleSchema = z.object({ /** Enable Tailscale Serve to expose gateway on tailnet. */ serve: z.boolean().default(false), /** Custom hostname for Tailscale Serve. Defaults to machine hostname. */ hostname: z.string().optional(), /** Tailscale Serve HTTPS port. */ port: z.number().default(443), }).default({}); const pairingSchema = z.object({ /** Enable DM pairing codes for unknown senders. */ enabled: z.boolean().default(false), /** Pairing code time-to-live duration (e.g. '5m', '1h'). */ code_ttl: z.string().default('5m'), /** Length of generated pairing codes. */ code_length: z.number().default(6), }).default({}); const serverSchema = z.object({ tailscale: tailscaleSchema, 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), /** Single-client gateway lock. When true, only one WebSocket client can be connected at a time. */ lock: z.boolean().default(false), }); /** All supported model provider identifiers. Used by the config schema and TUI autocompletion. */ export const MODEL_PROVIDERS = ['anthropic', 'openai', 'gemini', 'ollama', 'llamacpp', 'openrouter', 'bedrock', 'github', 'zhipuai', 'xai'] as const; export type ModelProvider = (typeof MODEL_PROVIDERS)[number]; const modelConfigBaseSchema = z.object({ provider: z.enum(MODEL_PROVIDERS), 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 modelConfigSchema = modelConfigBaseSchema.extend({ fallback: modelConfigBaseSchema.optional(), }); const thinkingSchema = z.object({ anthropic: z.object({ budgetTokens: z.number().default(4096), }).default({}), openai: z.object({ reasoningEffort: z.enum(['low', 'medium', 'high']).default('medium'), }).default({}), gemini: z.object({ budgetTokens: z.number().default(4096), }).default({}), }).default({}); 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(), thinking: thinkingSchema, }); 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 modelTierEnum = z.enum(['fast', 'default', 'complex', 'local']); 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(), model_tier: modelTierEnum.optional(), }); const webhookSchema = z.object({ name: z.string().min(1, 'Webhook name is required'), secret: z.string().optional(), message: z.string().default('{{body}}'), output: z.object({ channel: z.string().min(1), peer: z.string().min(1), }), enabled: z.boolean().default(true), }); const gmailSchema = z.object({ enabled: z.boolean().default(false), credentials_file: z.string().optional(), token_file: z.string().default('~/.config/flynn/gmail-token.json'), watch_labels: z.array(z.string()).default(['INBOX']), poll_interval: z.string().default('300s'), history_start: z.string().optional(), // ISO date string — only process emails after this date output: z.object({ channel: z.string().min(1), peer: z.string().min(1), }), message: z.string().default('New email from {{from}}: {{subject}}\n\n{{snippet}}'), }).optional(); const heartbeatCheckSchema = z.enum(['gateway', 'model', 'channels', 'memory', 'disk']); const heartbeatSchema = z.object({ enabled: z.boolean().default(false), interval: z.string().default('5m'), checks: z.array(heartbeatCheckSchema).default(['gateway', 'model', 'channels', 'memory', 'disk']), notify: z.object({ channel: z.string().min(1), peer: z.string().min(1), }).optional(), failure_threshold: z.number().min(1).max(10).default(2), disk_threshold_mb: z.number().min(10).default(100), }).default({}); const gcalSchema = z.object({ enabled: z.boolean().default(false), credentials_file: z.string().optional(), token_file: z.string().default('~/.config/flynn/gcal-token.json'), calendar_ids: z.array(z.string()).default(['primary']), }).optional(); const gdocsSchema = z.object({ enabled: z.boolean().default(false), credentials_file: z.string().optional(), token_file: z.string().default('~/.config/flynn/gdocs-token.json'), }).optional(); const gdriveSchema = z.object({ enabled: z.boolean().default(false), credentials_file: z.string().optional(), token_file: z.string().default('~/.config/flynn/gdrive-token.json'), }).optional(); const gtasksSchema = z.object({ enabled: z.boolean().default(false), credentials_file: z.string().optional(), token_file: z.string().default('~/.config/flynn/gtasks-token.json'), }).optional(); const automationSchema = z.object({ cron: z.array(cronJobSchema).default([]), webhooks: z.array(webhookSchema).default([]), gmail: gmailSchema, gcal: gcalSchema, gdocs: gdocsSchema, gdrive: gdriveSchema, gtasks: gtasksSchema, heartbeat: heartbeatSchema, }).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), /** Maximum tool-loop iterations before the agent stops. */ max_iterations: z.number().min(1).max(50).default(10), }).default({}); const embeddingProviderSchema = z.enum(['openai', 'gemini', 'ollama', 'llamacpp', 'voyage']); const embeddingSchema = z.object({ enabled: z.boolean().default(false), provider: embeddingProviderSchema.default('openai'), model: z.string().default('text-embedding-3-small'), endpoint: z.string().optional(), api_key: z.string().optional(), dimensions: z.number().optional(), chunk_size: z.number().min(64).max(8192).default(512), chunk_overlap: z.number().min(0).max(1024).default(50), top_k: z.number().min(1).max(50).default(5), hybrid_weight: z.number().min(0).max(1).default(0.7), }).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), embedding: embeddingSchema, }).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({}); const audioProviderSchema = z.object({ type: z.enum(['openai', 'groq', 'ollama', 'llamacpp', 'custom']), endpoint: z.string().optional(), api_key: z.string().optional(), model: z.string().optional(), }); const audioSchema = z.object({ enabled: z.boolean().default(false), provider: audioProviderSchema.optional(), }).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 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({}); const sessionsSchema = z.object({ ttl: z.string().default('30d'), }).default({}); const logLevelSchema = z.enum(['debug', 'info', 'warn', 'error', 'silent']).default('info'); const auditLevelSchema = z.enum(['debug', 'info', 'warn', 'error']).default('debug'); const auditSchema = z.object({ enabled: z.boolean().default(true), path: z.string().default('~/.local/share/flynn/audit.log'), max_size_mb: z.number().min(1).max(1000).default(10), keep_days: z.number().min(1).max(365).default(30), levels: z.object({ tools: auditLevelSchema.default('debug'), sessions: auditLevelSchema.default('debug'), automation: auditLevelSchema.default('debug'), }).default({}), }).default({}); export const configSchema = z.object({ log_level: logLevelSchema, audit: auditSchema, telegram: telegramSchema.optional(), 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, audio: audioSchema, prompt: promptSchema, tools: toolsSchema, sandbox: sandboxSchema, agent_configs: agentConfigsSchema, routing: routingSchema, sessions: sessionsSchema, pairing: pairingSchema, }); export type Config = z.infer; export type TelegramConfig = z.infer; export type ModelConfig = z.infer; export type CronJobConfig = z.infer; export type WebhookConfig = z.infer; export type GmailConfig = z.infer; export type AgentsConfig = z.infer; export type CompactionConfig = z.infer; export type MemoryConfig = z.infer; export type WebSearchConfig = z.infer; export type AudioConfig = 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; export type SessionsConfig = z.infer; export type ThinkingConfig = z.infer; export type HeartbeatConfig = z.infer; export type HeartbeatCheck = z.infer; export type EmbeddingConfig = z.infer; export type EmbeddingProvider = z.infer; export type GcalConfig = z.infer; export type GdocsConfig = z.infer; export type GdriveConfig = z.infer; export type GtasksConfig = z.infer; export type PairingCodeConfig = z.infer; export type LogLevel = z.infer; export type AuditConfig = z.infer; export type AuditLevel = z.infer;