Files
flynn/src/config/schema.ts
T

1081 lines
43 KiB
TypeScript

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(false),
});
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 laneQueueSchema = z.object({
/** Queue behavior for concurrent requests in the same session lane. */
mode: z.enum(['collect', 'followup', 'steer', 'steer_backlog', 'interrupt']).default('collect'),
/** Max queued (pending) requests per lane. */
cap: z.number().min(1).max(1000).default(50),
/** Overflow strategy when cap is reached. */
overflow: z.enum(['drop_old', 'drop_new']).default('drop_old'),
/** Debounce window before starting next queued request (ms). */
debounce_ms: z.number().min(0).max(60_000).default(0),
/** Include contextual summary details in overflow rejections. */
summarize_overflow: z.boolean().default(true),
/** Optional per-channel/per-session queue policy overrides. */
overrides: z.object({
channels: z.record(
z.string(),
z.object({
mode: z.enum(['collect', 'followup', 'steer', 'steer_backlog', 'interrupt']).optional(),
cap: z.number().min(1).max(1000).optional(),
overflow: z.enum(['drop_old', 'drop_new']).optional(),
debounce_ms: z.number().min(0).max(60_000).optional(),
summarize_overflow: z.boolean().optional(),
}),
).default({}),
sessions: z.record(
z.string(),
z.object({
mode: z.enum(['collect', 'followup', 'steer', 'steer_backlog', 'interrupt']).optional(),
cap: z.number().min(1).max(1000).optional(),
overflow: z.enum(['drop_old', 'drop_new']).optional(),
debounce_ms: z.number().min(0).max(60_000).optional(),
summarize_overflow: z.boolean().optional(),
}),
).default({}),
}).default({}),
}).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 serverWebchatPushSchema = z.object({
/** Enable WebChat web-push subscription endpoints and PWA metadata. */
enabled: z.boolean().default(false),
/** VAPID public key used by browser PushManager.subscribe(). */
vapid_public_key: z.string().optional(),
/** Soft cap for stored web-push subscriptions. */
max_subscriptions: z.number().min(1).max(50_000).default(5000),
}).default({});
const serverNodePolicySchema = z.object({
/** Enable node registration/capability RPC surface. */
enabled: z.boolean().default(false),
/** Allowed node roles for node.register. */
allowed_roles: z.array(z.string().min(1)).default(['companion']),
/** Optional feature gates exposed via system/node capability APIs. */
feature_gates: z.record(z.string(), z.boolean()).default({}),
/** Node location access controls. */
location: z.object({
/** Enable node.location.set/get and system.location visibility. */
enabled: z.boolean().default(false),
}).default({}),
/** Node push registration controls (e.g. APNs for iOS companion). */
push: z.object({
/** Enable node.push_token.set and push summary visibility in system.nodes. */
enabled: z.boolean().default(false),
}).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,
/** Per-session gateway lane queue behavior. */
queue: laneQueueSchema,
/** Optional companion-node registration/capability settings. */
nodes: serverNodePolicySchema,
/** Optional WebChat PWA push-subscription settings. */
webchat_push: serverWebchatPushSchema,
/** 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(),
api_keys: z.array(z.string().min(1)).optional(),
/** Cooldown (ms) before retrying a failed key/token profile in rotation pools. */
auth_profile_cooldown_ms: z.number().min(0).max(3_600_000).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({
default: z.enum(['claude_code', 'opencode', 'codex', 'gemini']).optional(),
claude_code: z.object({
enabled: z.boolean().default(false),
path: z.string().optional(),
args: z.array(z.string()).default([]),
timeout_ms: z.number().min(1_000).max(600_000).default(120_000),
}).default({ enabled: false }),
opencode: z.object({
enabled: z.boolean().default(false),
path: z.string().optional(),
args: z.array(z.string()).default([]),
timeout_ms: z.number().min(1_000).max(600_000).default(120_000),
}).default({ enabled: false }),
codex: z.object({
enabled: z.boolean().default(false),
path: z.string().optional(),
args: z.array(z.string()).default([]),
timeout_ms: z.number().min(1_000).max(600_000).default(120_000),
}).default({ enabled: false }),
gemini: z.object({
enabled: z.boolean().default(false),
path: z.string().optional(),
args: z.array(z.string()).default([]),
timeout_ms: z.number().min(1_000).max(600_000).default(120_000),
}).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 reactionFilterSchema = z.object({
/** Case-insensitive substring match against inbound message text. */
contains: z.string().optional(),
/** Case-insensitive regex match against inbound message text. */
regex: z.string().optional(),
/** Dot-path metadata constraints (exact string comparison). */
metadata: z.record(z.string(), z.string()).optional(),
}).optional();
const automationReactionSchema = z.object({
name: z.string().min(1, 'Reaction name is required'),
enabled: z.boolean().default(true),
/** Source channels/events this rule applies to (e.g. gmail, webhook). */
on: z.array(z.string().min(1)).default([]),
filter: reactionFilterSchema,
/** Prompt template to run when matched. Supports {{text}}, {{channel}}, {{sender_id}}, {{metadata.*}}. */
run: z.string().min(1, 'Reaction run template is required'),
});
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(),
once_per_local_day: z.boolean().default(false),
});
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/<project-id>/topics/<topic>
* 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/<project-id>/subscriptions/<subscription>
*/
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', 'process_memory', 'backup', 'provider_errors']);
const heartbeatSchema = z.object({
enabled: z.boolean().default(false),
interval: z.string().default('5m'),
notify_cooldown: z.string().default('30m'),
checks: z.array(heartbeatCheckSchema).default(['gateway', 'model', 'channels', 'memory', 'disk', 'process_memory', 'backup', 'provider_errors']),
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),
process_memory_threshold_mb: z.number().min(64).default(1500),
backup_failure_threshold: z.number().min(1).max(10).default(1),
provider_error_rate_threshold: z.number().min(0).max(1).default(0.5),
provider_error_min_calls: z.number().min(1).default(5),
}).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 dailyBriefingSchema = z.object({
enabled: z.boolean().default(false),
name: z.string().min(1).default('daily-briefing'),
schedule: z.string().min(1).default('0 8 * * *'),
timezone: z.string().optional(),
dedupe_per_local_day: z.boolean().default(true),
output: z.object({
channel: z.string().min(1),
peer: z.string().min(1),
}).optional(),
prompt: z.string().min(1).default(
[
'Create my daily briefing.',
'',
'Use available tools to gather:',
'- Today\'s calendar events (calendar.today or calendar.list)',
'- Unread or recent important email (gmail.search/gmail.list)',
'- Top pending tasks (tasks.list/tasks.lists)',
'',
'Output format:',
'1) Schedule',
'2) Priorities',
'3) Risks/Follow-ups',
'4) Suggested first actions',
'',
'Keep it concise and actionable.',
].join('\n'),
),
model_tier: modelTierEnum.optional(),
}).default({});
const minioSyncTaskSchema = z.object({
prefix: z.string().min(1, 'MinIO sync prefix is required'),
bucket: z.string().optional(),
namespace_base: z.string().min(1).default('global/knowledge/minio'),
mode: z.enum(['append', 'replace']).default('append'),
max_objects: z.number().min(1).max(500).default(20),
max_chars_per_object: z.number().min(1).max(1_000_000).default(8_000),
force: z.boolean().default(false),
});
const minioSyncAutomationSchema = z.object({
enabled: z.boolean().default(false),
interval: z.string().default('6h'),
run_on_start: z.boolean().default(false),
tasks: z.array(minioSyncTaskSchema).default([]),
notify: z.object({
channel: z.string().min(1),
peer: z.string().min(1),
}).optional(),
notify_on_success: z.boolean().default(false),
}).default({});
const automationDeliveryModeSchema = z.enum(['shared_session', 'isolated_job', 'announce']);
const automationSchema = z.object({
/** Session strategy for automation-triggered runs (cron/webhooks/gmail). */
delivery_mode: automationDeliveryModeSchema.default('shared_session'),
reactions: z.array(automationReactionSchema).default([]),
cron: z.array(cronJobSchema).default([]),
webhooks: z.array(webhookSchema).default([]),
gmail: gmailSchema,
gcal: gcalSchema,
gdocs: gdocsSchema,
gdrive: gdriveSchema,
gtasks: gtasksSchema,
daily_briefing: dailyBriefingSchema,
minio_sync: minioSyncAutomationSchema,
heartbeat: heartbeatSchema,
}).default({});
const truthfulnessModeSchema = z.enum(['strict', 'standard', 'relaxed']);
const autonomyLevelSchema = z.enum(['conservative', 'standard', 'autonomous']);
const sensitiveModeSchema = z.enum(['deny_without_elevation', 'confirm_without_elevation']);
const immutableDenyRuleSchema = z.object({
tool: z.string().min(1),
args_pattern: z.string().min(1).optional(),
reason: z.string().min(1).optional(),
});
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',
}),
background_models: z.object({
compaction: z.object({
enabled: z.boolean().default(true),
provider: z.enum(MODEL_PROVIDERS),
model: z.string().min(1),
fallback_tier: modelTierEnum.default('fast'),
}).optional(),
memory_extraction: z.object({
enabled: z.boolean().default(true),
provider: z.enum(MODEL_PROVIDERS),
model: z.string().min(1),
fallback_tier: modelTierEnum.default('fast'),
}).optional(),
classification: z.object({
enabled: z.boolean().default(true),
provider: z.enum(MODEL_PROVIDERS),
model: z.string().min(1),
fallback_tier: modelTierEnum.default('fast'),
}).optional(),
tool_summarisation: z.object({
enabled: z.boolean().default(true),
provider: z.enum(MODEL_PROVIDERS),
model: z.string().min(1),
fallback_tier: modelTierEnum.default('fast'),
}).optional(),
complex_reasoning: z.object({
enabled: z.boolean().default(true),
provider: z.enum(MODEL_PROVIDERS),
model: z.string().min(1),
fallback_tier: modelTierEnum.default('fast'),
}).optional(),
}).default({}),
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'),
/** Sensitive host-action behavior for high-impact tools. */
sensitive_mode: sensitiveModeSchema.default('confirm_without_elevation'),
/** Immutable denylist enforced even during elevated mode. */
immutable_denylist: z.array(immutableDenyRuleSchema).default([
{
tool: 'shell.exec',
args_pattern: 'git push origin main',
reason: 'direct push to main is blocked by immutable policy',
},
{
tool: 'shell.exec',
args_pattern: 'git reset --hard',
reason: 'destructive hard reset is blocked by immutable policy',
},
{
tool: 'shell.exec',
args_pattern: 'git clean -fd',
reason: 'destructive clean is blocked by immutable policy',
},
]),
}).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),
proactive_extract: z.object({
enabled: z.boolean().default(false),
min_tool_calls: z.number().min(0).max(50).default(1),
namespace: z.string().default('global'),
}).default({}),
daily_log: z.object({
enabled: z.boolean().default(false),
namespace_prefix: z.string().default('daily'),
include_session_metadata: z.boolean().default(true),
max_user_chars: z.number().min(100).max(20000).default(2000),
max_assistant_chars: z.number().min(100).max(40000).default(4000),
}).default({}),
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),
proactive: z.object({
enabled: z.boolean().default(false),
warn_pct: z.number().min(10).max(100).default(75),
checkpoint_pct: z.number().min(10).max(100).default(85),
auto_compact_pct: z.number().min(10).max(100).default(95),
checkpoint_cooldown_ms: z.number().min(1000).max(86_400_000).default(300_000),
memory_namespace: z.string().default('session/checkpoints'),
}).default({}),
}).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 mattermostSchema = z.object({
server_url: z.string().url('Mattermost server_url must be a valid URL'),
bot_token: z.string().min(1, 'Mattermost bot_token is required'),
allowed_channel_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(3000),
}).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 bluebubblesSchema = z.object({
endpoint: z.string().url('BlueBubbles endpoint must be a valid URL'),
api_key: z.string().min(1, 'BlueBubbles api_key is required'),
webhook_token: z.string().optional(),
allowed_chat_guids: z.array(z.string()).default([]),
require_mention: z.boolean().default(true),
mention_name: z.string().default('flynn'),
}).optional();
const lineSchema = z.object({
channel_access_token: z.string().min(1, 'LINE channel_access_token is required'),
channel_secret: z.string().min(1, 'LINE channel_secret is required'),
allowed_source_ids: z.array(z.string()).default([]),
require_mention: z.boolean().default(true),
mention_name: z.string().default('flynn'),
minio: z.object({
enabled: z.boolean().default(false),
endpoint: z.string().optional(),
access_key: z.string().optional(),
secret_key: z.string().optional(),
bucket: z.string().optional(),
prefix: z.string().default('flynn/channels/line'),
secure: z.boolean().default(true),
expires: z.string().default('24h'),
mc_path: z.string().optional(),
}).default({}),
}).optional();
const feishuSchema = z.object({
app_id: z.string().min(1, 'Feishu app_id is required'),
app_secret: z.string().min(1, 'Feishu app_secret is required'),
webhook_token: z.string().optional(),
allowed_chat_ids: z.array(z.string()).default([]),
require_mention: z.boolean().default(true),
mention_name: z.string().default('flynn'),
endpoint: z.string().url('Feishu endpoint must be a valid URL').optional(),
}).optional();
const zaloSchema = z.object({
oa_access_token: z.string().min(1, 'Zalo oa_access_token is required'),
endpoint: z.string().url('Zalo endpoint must be a valid URL').optional(),
webhook_token: z.string().optional(),
allowed_user_ids: z.array(z.string()).default([]),
require_mention: z.boolean().default(true),
mention_name: z.string().default('flynn'),
minio: z.object({
enabled: z.boolean().default(false),
endpoint: z.string().optional(),
access_key: z.string().optional(),
secret_key: z.string().optional(),
bucket: z.string().optional(),
prefix: z.string().default('flynn/channels/zalo'),
secure: z.boolean().default(true),
expires: z.string().default('24h'),
mc_path: z.string().optional(),
}).default({}),
}).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 k8sSchema = z.object({
enabled: z.boolean().default(false),
kubectl_path: z.string().default('kubectl'),
default_namespace: z.string().optional(),
allowed_namespaces: z.array(z.string()).default([]),
}).optional();
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 talkModeSchema = z.object({
enabled: z.boolean().default(false),
wake_phrase: z.string().default('hey flynn'),
timeout_ms: z.number().min(1000).max(60 * 60 * 1000).default(120000),
allow_manual_toggle: z.boolean().default(true),
}).default({});
const audioSchema = z.object({
enabled: z.boolean().default(false),
provider: audioProviderSchema.optional(),
talk_mode: talkModeSchema,
}).default({});
const ttsOutputFormatSchema = z.enum(['mp3', 'wav', 'opus']);
const ttsProviderSchema = z.object({
type: z.enum(['openai', 'custom']).default('openai'),
endpoint: z.string().optional(),
api_key: z.string().optional(),
model: z.string().default('gpt-4o-mini-tts'),
voice: z.string().default('alloy'),
format: ttsOutputFormatSchema.default('mp3'),
});
const ttsSchema = z.object({
enabled: z.boolean().default(false),
/** Restrict voice replies to selected channels. Empty means all channels. */
enabled_channels: z.array(z.string().min(1)).default([]),
provider: ttsProviderSchema.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(),
backend: z.enum(['native', 'claude_code', 'opencode', 'codex', 'gemini']).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'),
end_summary: z.object({
enabled: z.boolean().default(false),
tier: modelTierEnum.default('fast'),
max_messages: z.number().min(2).max(500).default(50),
max_input_chars: z.number().min(500).max(200000).default(20000),
max_tokens: z.number().min(64).max(4096).default(512),
write_to_memory: z.boolean().default(true),
memory_namespace: z.string().default('session/summaries'),
}).default({}),
}).default({});
const backupSchema = z.object({
enabled: z.boolean().default(false),
schedule: z.string().optional(),
interval: z.string().default('24h'),
run_on_start: z.boolean().default(false),
notify: z.object({
channel: z.string().min(1),
peer: z.string().min(1),
}).optional(),
failure_threshold: z.number().min(1).max(10).default(1),
notify_recovery: z.boolean().default(true),
local_dir: z.string().default('~/.local/share/flynn/backups'),
include_vectors: z.boolean().default(true),
minio: z.object({
enabled: z.boolean().default(false),
endpoint: z.string().optional(),
access_key: z.string().optional(),
secret_key: z.string().optional(),
bucket: z.string().optional(),
prefix: z.string().default('flynn'),
secure: z.boolean().default(true),
mc_path: z.string().optional(),
}).default({}),
}).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,
mattermost: mattermostSchema,
teams: teamsSchema,
google_chat: googleChatSchema,
bluebubbles: bluebubblesSchema,
line: lineSchema,
feishu: feishuSchema,
zalo: zaloSchema,
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,
k8s: k8sSchema,
retry: retrySchema,
web_search: webSearchSchema,
audio: audioSchema,
tts: ttsSchema,
prompt: promptSchema,
tools: toolsSchema,
sandbox: sandboxSchema,
agent_configs: agentConfigsSchema,
routing: routingSchema,
intents: intentsSchema,
routing_policy: routingPolicySchema,
history_index: historyIndexSchema,
sessions: sessionsSchema,
backup: backupSchema,
pairing: pairingSchema,
});
export type Config = z.infer<typeof configSchema>;
export type TelegramConfig = z.infer<typeof telegramSchema>;
export type ModelConfig = z.infer<typeof modelConfigSchema>;
export type CronJobConfig = z.infer<typeof cronJobSchema>;
export type WebhookConfig = z.infer<typeof webhookSchema>;
export type GmailConfig = z.infer<typeof gmailSchema>;
export type AgentsConfig = z.infer<typeof agentsSchema>;
export type CompactionConfig = z.infer<typeof compactionSchema>;
export type MemoryConfig = z.infer<typeof memorySchema>;
export type WebSearchConfig = z.infer<typeof webSearchSchema>;
export type AudioConfig = z.infer<typeof audioSchema>;
export type TtsConfig = z.infer<typeof ttsSchema>;
export type ProcessConfig = z.infer<typeof processSchema>;
export type BrowserConfig = z.infer<typeof browserSchema>;
export type K8sConfig = z.infer<typeof k8sSchema>;
export type DiscordConfig = z.infer<typeof discordSchema>;
export type SlackConfig = z.infer<typeof slackSchema>;
export type WhatsAppConfig = z.infer<typeof whatsappSchema>;
export type MatrixConfig = z.infer<typeof matrixSchema>;
export type SignalConfig = z.infer<typeof signalSchema>;
export type MattermostConfig = z.infer<typeof mattermostSchema>;
export type TeamsConfig = z.infer<typeof teamsSchema>;
export type GoogleChatConfig = z.infer<typeof googleChatSchema>;
export type BlueBubblesConfig = z.infer<typeof bluebubblesSchema>;
export type RetryPolicyConfig = z.infer<typeof retrySchema>;
export type ContextLevel = z.infer<typeof contextLevelSchema>;
export type PromptConfig = z.infer<typeof promptSchema>;
export type ToolProfile = z.infer<typeof toolProfileEnum>;
export type ToolOverrideConfig = z.infer<typeof toolOverrideSchema>;
export type ToolsConfig = z.infer<typeof toolsSchema>;
export type SandboxConfig = z.infer<typeof sandboxSchema>;
export type AgentConfigEntry = z.infer<typeof agentConfigEntrySchema>;
export type RoutingConfig = z.infer<typeof routingSchema>;
export type IntentTargetType = z.infer<typeof intentTargetTypeSchema>;
export type IntentRuleConfig = z.infer<typeof intentRuleSchema>;
export type IntentsConfig = z.infer<typeof intentsSchema>;
export type RoutingPolicyConfig = z.infer<typeof routingPolicySchema>;
export type HistoryIndexConfig = z.infer<typeof historyIndexSchema>;
export type ServerConfig = z.infer<typeof serverSchema>;
export type SessionsConfig = z.infer<typeof sessionsSchema>;
export type BackupConfig = z.infer<typeof backupSchema>;
export type ThinkingConfig = z.infer<typeof thinkingSchema>;
export type HeartbeatConfig = z.infer<typeof heartbeatSchema>;
export type HeartbeatCheck = z.infer<typeof heartbeatCheckSchema>;
export type EmbeddingConfig = z.infer<typeof embeddingSchema>;
export type EmbeddingProvider = z.infer<typeof embeddingProviderSchema>;
export type QmdConfig = z.infer<typeof qmdSchema>;
export type GcalConfig = z.infer<typeof gcalSchema>;
export type GdocsConfig = z.infer<typeof gdocsSchema>;
export type GdriveConfig = z.infer<typeof gdriveSchema>;
export type GtasksConfig = z.infer<typeof gtasksSchema>;
export type DailyBriefingConfig = z.infer<typeof dailyBriefingSchema>;
export type MinioSyncTaskConfig = z.infer<typeof minioSyncTaskSchema>;
export type MinioSyncAutomationConfig = z.infer<typeof minioSyncAutomationSchema>;
export type AutomationDeliveryMode = z.infer<typeof automationDeliveryModeSchema>;
export type AutomationReactionConfig = z.infer<typeof automationReactionSchema>;
export type PairingCodeConfig = z.infer<typeof pairingSchema>;
export type LogLevel = z.infer<typeof logLevelSchema>;
export type AuditConfig = z.infer<typeof auditSchema>;
export type AuditLevel = z.infer<typeof auditLevelSchema>;
export type TruthfulnessMode = z.infer<typeof truthfulnessModeSchema>;
export type AutonomyLevel = z.infer<typeof autonomyLevelSchema>;
export type SensitiveMode = z.infer<typeof sensitiveModeSchema>;
export type ImmutableDenyRule = z.infer<typeof immutableDenyRuleSchema>;