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 wsRateLimitSchema = z.object({ enabled: z.boolean().default(true), capacity: z.number().min(1).max(1000).default(30), refill_per_sec: z.number().min(1).max(1000).default(15), max_violations: z.number().min(1).max(100).default(8), violation_window_ms: z.number().min(1000).max(60000).default(10000), }).default({}); const serverDiscoverySchema = z.object({ /** Enable local-network service discovery (mDNS/Bonjour advertisement). */ enabled: z.boolean().default(false), /** Service instance name advertised on LAN. */ service_name: z.string().min(1).default('flynn-gateway'), /** mDNS service type. */ service_type: z.string().min(1).default('_flynn._tcp'), /** Additional TXT metadata advertised with the service record. */ txt: z.record(z.string(), z.string()).default({}), }).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), /** Maximum size (bytes) for inbound HTTP request bodies (webhooks/Gmail push). */ max_request_body_bytes: z.number().min(1024).max(10 * 1024 * 1024).default(1_048_576), /** Per-connection WebSocket ingress rate limit settings. */ ws_rate_limit: wsRateLimitSchema, /** Optional Bonjour/mDNS advertisement settings. */ discovery: serverDiscoverySchema, }); /** All supported model provider identifiers. Used by the config schema and TUI autocompletion. */ export const MODEL_PROVIDERS = ['anthropic', 'openai', 'gemini', 'ollama', 'llamacpp', 'openrouter', 'vercel', 'bedrock', 'github', 'zhipuai', 'xai', 'minimax', 'moonshot', 'synthetic'] 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(), /** Credential selection strategy for this tier (provider-specific). */ auth_mode: z.enum(['auto', 'api_key', 'oauth']).optional(), /** Use OAuth credential flow (provider-specific). */ use_oauth: z.boolean().optional(), for: z.array(z.string()).optional(), num_gpu: z.number().optional(), context_window: z.number().optional(), supports_audio: z.boolean().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 skillsLoadSchema = z.object({ /** Enable filesystem watcher for automatic skill reload detection. */ watch: z.boolean().default(false), /** Debounce window for batched watcher events. */ watch_debounce_ms: z.number().min(10).max(10_000).default(250), }).default({}); const skillsShellRunnerGovernanceSchema = z.object({ /** Responsible owner for shell-runner allowlist decisions. */ owner: z.string().min(1).optional(), /** Review cadence for allowlist + rollout status checks. */ review_cadence_days: z.number().min(1).max(90).default(7), /** Minimum success rate required before broader rollout. */ promotion_min_success_rate: z.number().min(0).max(1).default(0.9), }).default({}); const skillsSchema = z.object({ /** Registry catalog source for `flynn skills registry` and install-by-id (file path or HTTPS URL). */ registry_source: z.string().optional(), /** 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(), /** Global policy gate for installer command execution. */ installation_execution: z.enum(['disabled', 'enabled']).default('disabled'), /** Allow use of the shell runner for installer commands. */ allow_shell_runner: z.boolean().default(false), /** Allowlist patterns for shell runner commands (supports '*' wildcard). */ shell_runner_allowlist: z.array(z.string()).default([]), /** Governance controls for shell-runner rollout decisions. */ shell_runner_governance: skillsShellRunnerGovernanceSchema, /** Skills watcher settings. */ load: skillsLoadSchema, }).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'), /** * Optional Google Cloud Pub/Sub topic for Gmail push notifications. * Format: projects//topics/ * If omitted, push notifications are disabled and Flynn will use polling. */ pubsub_topic: z.string().optional(), /** * Explicitly disable Gmail push watch registration even if pubsub_topic is set. * Useful for environments where Google cannot reach the gateway (e.g. tailnet-only). */ disable_push: z.boolean().default(false), /** * Optional Pub/Sub subscription for pull-based delivery (no inbound webhook required). * Format: projects//subscriptions/ */ pubsub_subscription_id: z.string().optional(), /** How often to pull messages from pubsub_subscription_id (e.g. '60s'). */ pubsub_pull_interval: z.string().default('60s'), /** Max messages to pull per cycle (1..100). */ pubsub_max_messages: z.number().min(1).max(100).default(10), 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 automationDeliveryModeSchema = z.enum(['shared_session', 'isolated_job']); const automationSchema = z.object({ /** Session strategy for automation-triggered runs (cron/webhooks/gmail). */ delivery_mode: automationDeliveryModeSchema.default('shared_session'), cron: z.array(cronJobSchema).default([]), webhooks: z.array(webhookSchema).default([]), gmail: gmailSchema, gcal: gcalSchema, gdocs: gdocsSchema, gdrive: gdriveSchema, gtasks: gtasksSchema, heartbeat: heartbeatSchema, }).default({}); const truthfulnessModeSchema = z.enum(['strict', 'standard', 'relaxed']); const autonomyLevelSchema = z.enum(['conservative', 'standard', 'autonomous']); 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(200).default(200), /** Truthfulness enforcement level: strict | standard | relaxed. */ truthfulness_mode: truthfulnessModeSchema.default('standard'), /** Autonomy level for tool execution: conservative | standard | autonomous. */ autonomy_level: autonomyLevelSchema.default('standard'), }).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 qmdSchema = z.object({ /** Enable experimental QMD (query markdown database) memory search backend. */ enabled: z.boolean().default(false), /** Maximum number of QMD results returned by memory.search. */ top_k: z.number().min(1).max(50).default(8), /** Minimum relevance score (0-1) for QMD matches. */ min_score: z.number().min(0).max(1).default(0.15), }).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), injection_strategy: z.enum(['all', 'recent', 'adaptive']).default('all'), max_injection_tokens: z.number().min(100).max(10000).default(2000), max_context_tokens: z.number().min(100).max(10000).default(2000), embedding: embeddingSchema, qmd: qmdSchema, }).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), importance_threshold: z.number().min(0).max(1).default(1), }).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(), no_sandbox: z.boolean().default(false), }).optional(); const matrixSchema = z.object({ homeserver_url: z.string().url('Homeserver URL is required'), access_token: z.string().min(1, 'Access token is required'), allowed_room_ids: z.array(z.string()).default([]), require_mention: z.boolean().default(true), sync_timeout_ms: z.number().min(1000).max(120000).default(30000), display_name: z.string().optional(), }).optional(); const signalSchema = z.object({ account: z.string().min(1, 'Signal account is required'), signal_cli_path: z.string().default('signal-cli'), allowed_numbers: z.array(z.string()).default([]), allowed_group_ids: z.array(z.string()).default([]), require_mention: z.boolean().default(true), mention_name: z.string().default('flynn'), poll_interval_ms: z.number().min(1000).max(60000).default(5000), send_timeout_ms: z.number().min(1000).max(60000).default(15000), }).optional(); const teamsSchema = z.object({ app_id: z.string().min(1, 'Teams app_id is required'), app_password: z.string().min(1, 'Teams app_password is required'), allowed_conversation_ids: z.array(z.string()).default([]), require_mention: z.boolean().default(true), }).optional(); const googleChatSchema = z.object({ service_account_key_file: z.string().optional(), service_account_json: z.string().optional(), webhook_token: z.string().optional(), allowed_space_names: z.array(z.string()).default([]), require_mention: z.boolean().default(true), }).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 intentTargetTypeSchema = z.enum(['agent', 'skill']); const intentRuleSchema = z.object({ name: z.string().min(1), patterns: z.array(z.string().min(1)).min(1), target: z.object({ type: intentTargetTypeSchema, name: z.string().min(1), }), priority: z.number().default(0), enabled: z.boolean().default(true), }); const intentsSchema = z.object({ enabled: z.boolean().default(false), match_threshold: z.number().min(0).max(1).default(0.7), rules: z.array(intentRuleSchema).default([]), }).default({}); const routingPolicySchema = z.object({ enabled: z.boolean().default(false), fast_path_threshold: z.number().min(0).max(1).default(0.85), llm_threshold: z.number().min(0).max(1).default(0.5), default_path: z.enum(['fast', 'llm']).default('llm'), }).default({}); const contextLevelSchema = z.enum(['minimal', 'normal', 'detailed', 'debug']); 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([]), /** Prompt context depth control: minimal | normal | detailed | debug. */ context_level: contextLevelSchema.default('normal'), }).default({}); const sessionsSchema = z.object({ ttl: z.string().default('30d'), }).default({}); const historyIndexSchema = z.object({ enabled: z.boolean().default(false), max_keywords: z.number().min(1).max(20).default(8), search_limit: z.number().min(1).max(100).default(10), min_score: z.number().min(0).max(1).default(0.15), routing_boost: z.number().min(0).max(0.2).default(0.05), }).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, matrix: matrixSchema, signal: signalSchema, teams: teamsSchema, google_chat: googleChatSchema, 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, intents: intentsSchema, routing_policy: routingPolicySchema, history_index: historyIndexSchema, 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 MatrixConfig = z.infer; export type SignalConfig = z.infer; export type TeamsConfig = z.infer; export type GoogleChatConfig = z.infer; export type RetryPolicyConfig = z.infer; export type ContextLevel = 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 IntentTargetType = z.infer; export type IntentRuleConfig = z.infer; export type IntentsConfig = z.infer; export type RoutingPolicyConfig = z.infer; export type HistoryIndexConfig = 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 QmdConfig = z.infer; export type GcalConfig = z.infer; export type GdocsConfig = z.infer; export type GdriveConfig = z.infer; export type GtasksConfig = z.infer; export type AutomationDeliveryMode = z.infer; export type PairingCodeConfig = z.infer; export type LogLevel = z.infer; export type AuditConfig = z.infer; export type AuditLevel = z.infer; export type TruthfulnessMode = z.infer; export type AutonomyLevel = z.infer;