import type { GatewayRequest, OutboundMessage } from '../protocol.js'; import { makeResponse, makeError, ErrorCode } from '../protocol.js'; import { MODEL_PROVIDERS, type Config, type ModelProvider } from '../../config/index.js'; import type { ModelRouter, ModelTier } from '../../models/router.js'; import { createClientFromConfig } from '../../daemon/models.js'; export interface ConfigHandlerDeps { config: Config; persistConfig?: (nextConfig: Config) => Promise | void; modelRouter?: ModelRouter; } function ensureCouncilsConfig(config: Config): NonNullable { config.councils ??= { enabled: false, defaults: { max_rounds: 2, ideas_per_round: 6, top_ideas_for_bridge: 3, bridge_packet_max_chars: 2500, bridge_field_max_bullets: 6, bridge_entry_max_chars: 300, novelty_delta_threshold: 10, repetition_threshold: 70, }, strict_grounding: false, strict_meta_validation: true, groups: { D: { arbiter_agent: 'council_d_arbiter', freethinker_agent: 'council_d_freethinker', model_tier: 'complex', group_prompt_prefix: 'Optimize for feasibility and speed-to-test. Prefer boring-but-true.', novelty_bias: 'low', risk_tolerance: 'low', forbidden_approaches: ['moonshots', 'handwavy AI claims', 'unverified assumptions'], }, P: { arbiter_agent: 'council_p_arbiter', freethinker_agent: 'council_p_freethinker', model_tier: 'complex', group_prompt_prefix: 'Optimize for reframing and non-obvious leverage. Weird is fine; label speculation.', novelty_bias: 'high', risk_tolerance: 'high', forbidden_approaches: ['incremental tweaks', 'obvious best practices', 'purely conventional solutions'], }, }, meta_arbiter_agent: 'council_meta_arbiter', meta_model_tier: 'complex', }; return config.councils; } /** * Redact sensitive values from config before returning. * Replaces API keys, tokens, passwords, and other credentials with "***". * * Covers: telegram, discord, slack, matrix, mattermost, server, models (tiers + fallbacks + local_providers), * web_search, audio, memory.embedding, automation (webhooks + gmail), and mcp server env vars. */ export function redactConfig(config: Config): Record { const raw = JSON.parse(JSON.stringify(config)) as Record; // Helper: redact specified keys on an object if they exist and are non-nullish const redact = (obj: Record | undefined, ...keys: string[]) => { if (!obj) {return;} for (const key of keys) { if (obj[key] !== undefined && obj[key] !== null) {obj[key] = '***';} } }; // Telegram redact(raw.telegram as Record, 'bot_token'); // Discord redact(raw.discord as Record, 'bot_token'); // Slack redact(raw.slack as Record, 'bot_token', 'app_token', 'signing_secret'); // Matrix redact(raw.matrix as Record, 'access_token'); // Mattermost redact(raw.mattermost as Record, 'bot_token'); // Server (gateway bearer token) redact(raw.server as Record, 'token'); // Models — tiers, their fallbacks, and local_providers (+ their fallbacks) const models = raw.models as Record | undefined; if (models) { for (const tier of ['default', 'fast', 'complex', 'local']) { const m = models[tier] as Record | undefined; redact(m, 'api_key', 'auth_token'); const fb = m?.fallback as Record | undefined; redact(fb, 'api_key', 'auth_token'); } const localProviders = models.local_providers as Record> | undefined; if (localProviders) { for (const provider of Object.values(localProviders)) { redact(provider, 'api_key', 'auth_token'); const fb = provider.fallback as Record | undefined; redact(fb, 'api_key', 'auth_token'); } } } // Web search redact(raw.web_search as Record, 'api_key'); // Audio redact(raw.audio as Record, 'transcription_api_key'); // Memory → embedding const memory = raw.memory as Record | undefined; if (memory) { redact(memory.embedding as Record, 'api_key'); } // Automation — webhook HMAC secrets and gmail credential paths const automation = raw.automation as Record | undefined; if (automation) { const webhooks = automation.webhooks as Record[] | undefined; if (webhooks) { for (const wh of webhooks) { redact(wh, 'secret'); } } const gmail = automation.gmail as Record | undefined; redact(gmail, 'credentials_file', 'token_file'); } // MCP server env vars (may contain API keys or other secrets) const mcp = raw.mcp as Record | undefined; if (mcp) { const servers = mcp.servers as Record[] | undefined; if (servers) { for (const srv of servers) { if (srv.env && typeof srv.env === 'object') { const env = srv.env as Record; for (const key of Object.keys(env)) { env[key] = '***'; } } } } } return raw; } /** Keys that are safe to update at runtime via config.patch. */ const PATCHABLE_KEYS: Record boolean> = { 'hooks.confirm': (config, value) => { if (!Array.isArray(value) || !value.every((v) => typeof v === 'string')) {return false;} config.hooks.confirm = value as string[]; return true; }, 'hooks.log': (config, value) => { if (!Array.isArray(value) || !value.every((v) => typeof v === 'string')) {return false;} config.hooks.log = value as string[]; return true; }, 'hooks.silent': (config, value) => { if (!Array.isArray(value) || !value.every((v) => typeof v === 'string')) {return false;} config.hooks.silent = value as string[]; return true; }, 'server.localhost': (config, value) => { if (typeof value !== 'boolean') {return false;} config.server.localhost = value; return true; }, 'server.queue.mode': (config, value) => { if (!['collect', 'followup', 'steer', 'steer_backlog', 'interrupt'].includes(String(value))) {return false;} config.server.queue.mode = value as typeof config.server.queue.mode; return true; }, 'server.queue.cap': (config, value) => { if (typeof value !== 'number' || !Number.isFinite(value) || value < 1 || value > 1000) {return false;} config.server.queue.cap = Math.floor(value); return true; }, 'server.queue.overflow': (config, value) => { if (value !== 'drop_old' && value !== 'drop_new') {return false;} config.server.queue.overflow = value; return true; }, 'server.queue.debounce_ms': (config, value) => { if (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > 60_000) {return false;} config.server.queue.debounce_ms = Math.floor(value); return true; }, 'server.queue.summarize_overflow': (config, value) => { if (typeof value !== 'boolean') {return false;} config.server.queue.summarize_overflow = value; return true; }, 'server.nodes.location.enabled': (config, value) => { if (typeof value !== 'boolean') {return false;} config.server.nodes.location.enabled = value; return true; }, 'server.nodes.push.enabled': (config, value) => { if (typeof value !== 'boolean') {return false;} config.server.nodes.push.enabled = value; return true; }, 'agents.primary_tier': (config, value) => { if (value !== 'fast' && value !== 'default' && value !== 'complex' && value !== 'local') {return false;} config.agents.primary_tier = value; return true; }, 'agents.delegation.compaction': (config, value) => { if (value !== 'fast' && value !== 'default' && value !== 'complex' && value !== 'local') {return false;} config.agents.delegation.compaction = value; return true; }, 'agents.delegation.memory_extraction': (config, value) => { if (value !== 'fast' && value !== 'default' && value !== 'complex' && value !== 'local') {return false;} config.agents.delegation.memory_extraction = value; return true; }, 'agents.delegation.classification': (config, value) => { if (value !== 'fast' && value !== 'default' && value !== 'complex' && value !== 'local') {return false;} config.agents.delegation.classification = value; return true; }, 'agents.delegation.tool_summarisation': (config, value) => { if (value !== 'fast' && value !== 'default' && value !== 'complex' && value !== 'local') {return false;} config.agents.delegation.tool_summarisation = value; return true; }, 'agents.delegation.complex_reasoning': (config, value) => { if (value !== 'fast' && value !== 'default' && value !== 'complex' && value !== 'local') {return false;} config.agents.delegation.complex_reasoning = value; return true; }, 'agents.background_models.compaction.enabled': (config, value) => { if (typeof value !== 'boolean') {return false;} config.agents.background_models.compaction ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' }; config.agents.background_models.compaction.enabled = value; return true; }, 'agents.background_models.compaction.provider': (config, value) => { if (!MODEL_PROVIDERS.includes(String(value) as ModelProvider)) {return false;} config.agents.background_models.compaction ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' }; config.agents.background_models.compaction.provider = value as ModelProvider; return true; }, 'agents.background_models.compaction.model': (config, value) => { if (typeof value !== 'string' || value.trim().length === 0) {return false;} config.agents.background_models.compaction ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' }; config.agents.background_models.compaction.model = value.trim(); return true; }, 'agents.background_models.compaction.fallback_tier': (config, value) => { if (value !== 'fast' && value !== 'default' && value !== 'complex' && value !== 'local') {return false;} config.agents.background_models.compaction ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' }; config.agents.background_models.compaction.fallback_tier = value; return true; }, 'agents.background_models.memory_extraction.enabled': (config, value) => { if (typeof value !== 'boolean') {return false;} config.agents.background_models.memory_extraction ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' }; config.agents.background_models.memory_extraction.enabled = value; return true; }, 'agents.background_models.memory_extraction.provider': (config, value) => { if (!MODEL_PROVIDERS.includes(String(value) as ModelProvider)) {return false;} config.agents.background_models.memory_extraction ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' }; config.agents.background_models.memory_extraction.provider = value as ModelProvider; return true; }, 'agents.background_models.memory_extraction.model': (config, value) => { if (typeof value !== 'string' || value.trim().length === 0) {return false;} config.agents.background_models.memory_extraction ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' }; config.agents.background_models.memory_extraction.model = value.trim(); return true; }, 'agents.background_models.memory_extraction.fallback_tier': (config, value) => { if (value !== 'fast' && value !== 'default' && value !== 'complex' && value !== 'local') {return false;} config.agents.background_models.memory_extraction ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' }; config.agents.background_models.memory_extraction.fallback_tier = value; return true; }, 'agents.background_models.classification.enabled': (config, value) => { if (typeof value !== 'boolean') {return false;} config.agents.background_models.classification ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' }; config.agents.background_models.classification.enabled = value; return true; }, 'agents.background_models.classification.provider': (config, value) => { if (!MODEL_PROVIDERS.includes(String(value) as ModelProvider)) {return false;} config.agents.background_models.classification ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' }; config.agents.background_models.classification.provider = value as ModelProvider; return true; }, 'agents.background_models.classification.model': (config, value) => { if (typeof value !== 'string' || value.trim().length === 0) {return false;} config.agents.background_models.classification ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' }; config.agents.background_models.classification.model = value.trim(); return true; }, 'agents.background_models.classification.fallback_tier': (config, value) => { if (value !== 'fast' && value !== 'default' && value !== 'complex' && value !== 'local') {return false;} config.agents.background_models.classification ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' }; config.agents.background_models.classification.fallback_tier = value; return true; }, 'agents.background_models.tool_summarisation.enabled': (config, value) => { if (typeof value !== 'boolean') {return false;} config.agents.background_models.tool_summarisation ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' }; config.agents.background_models.tool_summarisation.enabled = value; return true; }, 'agents.background_models.tool_summarisation.provider': (config, value) => { if (!MODEL_PROVIDERS.includes(String(value) as ModelProvider)) {return false;} config.agents.background_models.tool_summarisation ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' }; config.agents.background_models.tool_summarisation.provider = value as ModelProvider; return true; }, 'agents.background_models.tool_summarisation.model': (config, value) => { if (typeof value !== 'string' || value.trim().length === 0) {return false;} config.agents.background_models.tool_summarisation ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' }; config.agents.background_models.tool_summarisation.model = value.trim(); return true; }, 'agents.background_models.tool_summarisation.fallback_tier': (config, value) => { if (value !== 'fast' && value !== 'default' && value !== 'complex' && value !== 'local') {return false;} config.agents.background_models.tool_summarisation ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' }; config.agents.background_models.tool_summarisation.fallback_tier = value; return true; }, 'agents.background_models.complex_reasoning.enabled': (config, value) => { if (typeof value !== 'boolean') {return false;} config.agents.background_models.complex_reasoning ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' }; config.agents.background_models.complex_reasoning.enabled = value; return true; }, 'agents.background_models.complex_reasoning.provider': (config, value) => { if (!MODEL_PROVIDERS.includes(String(value) as ModelProvider)) {return false;} config.agents.background_models.complex_reasoning ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' }; config.agents.background_models.complex_reasoning.provider = value as ModelProvider; return true; }, 'agents.background_models.complex_reasoning.model': (config, value) => { if (typeof value !== 'string' || value.trim().length === 0) {return false;} config.agents.background_models.complex_reasoning ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' }; config.agents.background_models.complex_reasoning.model = value.trim(); return true; }, 'agents.background_models.complex_reasoning.fallback_tier': (config, value) => { if (value !== 'fast' && value !== 'default' && value !== 'complex' && value !== 'local') {return false;} config.agents.background_models.complex_reasoning ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' }; config.agents.background_models.complex_reasoning.fallback_tier = value; return true; }, 'councils.enabled': (config, value) => { if (typeof value !== 'boolean') {return false;} const councils = ensureCouncilsConfig(config); councils.enabled = value; return true; }, 'councils.defaults.max_rounds': (config, value) => { if (typeof value !== 'number' || !Number.isFinite(value) || value < 1 || value > 6) {return false;} const councils = ensureCouncilsConfig(config); councils.defaults.max_rounds = Math.floor(value); return true; }, 'councils.groups.D.model_tier': (config, value) => { if (value !== 'fast' && value !== 'default' && value !== 'complex' && value !== 'local') {return false;} const councils = ensureCouncilsConfig(config); councils.groups.D.model_tier = value; return true; }, 'councils.groups.P.model_tier': (config, value) => { if (value !== 'fast' && value !== 'default' && value !== 'complex' && value !== 'local') {return false;} const councils = ensureCouncilsConfig(config); councils.groups.P.model_tier = value; return true; }, 'councils.meta_model_tier': (config, value) => { if (value !== 'fast' && value !== 'default' && value !== 'complex' && value !== 'local') {return false;} const councils = ensureCouncilsConfig(config); councils.meta_model_tier = value; return true; }, 'councils.groups.D.arbiter_agent': (config, value) => { if (typeof value !== 'string' || value.trim().length === 0) {return false;} const councils = ensureCouncilsConfig(config); councils.groups.D.arbiter_agent = value.trim(); return true; }, 'councils.groups.D.freethinker_agent': (config, value) => { if (typeof value !== 'string' || value.trim().length === 0) {return false;} const councils = ensureCouncilsConfig(config); councils.groups.D.freethinker_agent = value.trim(); return true; }, 'councils.groups.D.grounder_agent': (config, value) => { if (typeof value !== 'string') {return false;} const next = value.trim(); const councils = ensureCouncilsConfig(config); councils.groups.D.grounder_agent = next.length > 0 ? next : undefined; return true; }, 'councils.groups.P.arbiter_agent': (config, value) => { if (typeof value !== 'string' || value.trim().length === 0) {return false;} const councils = ensureCouncilsConfig(config); councils.groups.P.arbiter_agent = value.trim(); return true; }, 'councils.groups.P.freethinker_agent': (config, value) => { if (typeof value !== 'string' || value.trim().length === 0) {return false;} const councils = ensureCouncilsConfig(config); councils.groups.P.freethinker_agent = value.trim(); return true; }, 'councils.groups.P.grounder_agent': (config, value) => { if (typeof value !== 'string') {return false;} const next = value.trim(); const councils = ensureCouncilsConfig(config); councils.groups.P.grounder_agent = next.length > 0 ? next : undefined; return true; }, 'councils.meta_arbiter_agent': (config, value) => { if (typeof value !== 'string' || value.trim().length === 0) {return false;} const councils = ensureCouncilsConfig(config); councils.meta_arbiter_agent = value.trim(); return true; }, 'councils.scaffold_path': (config, value) => { if (typeof value !== 'string') {return false;} const next = value.trim(); const councils = ensureCouncilsConfig(config); councils.scaffold_path = next.length > 0 ? next : undefined; return true; }, 'models.default.provider': (config, value) => { if (!MODEL_PROVIDERS.includes(String(value) as ModelProvider)) {return false;} config.models.default.provider = value as ModelProvider; return true; }, 'models.default.model': (config, value) => { if (typeof value !== 'string' || value.trim().length === 0) {return false;} config.models.default.model = value.trim(); return true; }, 'models.fast.provider': (config, value) => { if (!MODEL_PROVIDERS.includes(String(value) as ModelProvider)) {return false;} config.models.fast ??= { ...config.models.default }; config.models.fast.provider = value as ModelProvider; return true; }, 'models.fast.model': (config, value) => { if (typeof value !== 'string' || value.trim().length === 0) {return false;} config.models.fast ??= { ...config.models.default }; config.models.fast.model = value.trim(); return true; }, 'models.complex.provider': (config, value) => { if (!MODEL_PROVIDERS.includes(String(value) as ModelProvider)) {return false;} config.models.complex ??= { ...config.models.default }; config.models.complex.provider = value as ModelProvider; return true; }, 'models.complex.model': (config, value) => { if (typeof value !== 'string' || value.trim().length === 0) {return false;} config.models.complex ??= { ...config.models.default }; config.models.complex.model = value.trim(); return true; }, 'models.local.provider': (config, value) => { if (!MODEL_PROVIDERS.includes(String(value) as ModelProvider)) {return false;} config.models.local ??= { ...config.models.default }; config.models.local.provider = value as ModelProvider; return true; }, 'models.local.model': (config, value) => { if (typeof value !== 'string' || value.trim().length === 0) {return false;} config.models.local ??= { ...config.models.default }; config.models.local.model = value.trim(); return true; }, 'automation.delivery_mode': (config, value) => { if (value !== 'shared_session' && value !== 'isolated_job' && value !== 'announce') {return false;} config.automation ??= {} as Config['automation']; config.automation.delivery_mode = value; return true; }, 'automation.daily_briefing.enabled': (config, value) => { if (typeof value !== 'boolean') {return false;} config.automation ??= {} as Config['automation']; config.automation.daily_briefing ??= {} as Config['automation']['daily_briefing']; config.automation.daily_briefing.enabled = value; return true; }, 'automation.daily_briefing.output.channel': (config, value) => { if (typeof value !== 'string' || value.trim().length === 0) {return false;} config.automation ??= {} as Config['automation']; config.automation.daily_briefing ??= {} as Config['automation']['daily_briefing']; config.automation.daily_briefing.output ??= { channel: 'webchat', peer: 'assistant' }; config.automation.daily_briefing.output.channel = value.trim(); return true; }, 'automation.daily_briefing.output.peer': (config, value) => { if (typeof value !== 'string' || value.trim().length === 0) {return false;} config.automation ??= {} as Config['automation']; config.automation.daily_briefing ??= {} as Config['automation']['daily_briefing']; config.automation.daily_briefing.output ??= { channel: 'webchat', peer: 'assistant' }; config.automation.daily_briefing.output.peer = value.trim(); return true; }, 'automation.daily_briefing.prompt': (config, value) => { if (typeof value !== 'string' || value.trim().length === 0) {return false;} config.automation ??= {} as Config['automation']; config.automation.daily_briefing ??= {} as Config['automation']['daily_briefing']; config.automation.daily_briefing.prompt = value; return true; }, 'automation.daily_briefing.schedule': (config, value) => { if (typeof value !== 'string' || value.trim().length === 0) {return false;} config.automation ??= {} as Config['automation']; config.automation.daily_briefing ??= {} as Config['automation']['daily_briefing']; config.automation.daily_briefing.schedule = value.trim(); return true; }, 'automation.daily_briefing.timezone': (config, value) => { if (typeof value !== 'string' || value.trim().length === 0) {return false;} config.automation ??= {} as Config['automation']; config.automation.daily_briefing ??= {} as Config['automation']['daily_briefing']; config.automation.daily_briefing.timezone = value.trim(); return true; }, 'automation.daily_briefing.model_tier': (config, value) => { if (value !== 'fast' && value !== 'default' && value !== 'complex' && value !== 'local') {return false;} config.automation ??= {} as Config['automation']; config.automation.daily_briefing ??= {} as Config['automation']['daily_briefing']; config.automation.daily_briefing.model_tier = value; return true; }, 'automation.gmail.enabled': (config, value) => { if (typeof value !== 'boolean') {return false;} config.automation ??= {} as Config['automation']; config.automation.gmail ??= {} as NonNullable; config.automation.gmail.enabled = value; return true; }, 'automation.gcal.enabled': (config, value) => { if (typeof value !== 'boolean') {return false;} config.automation ??= {} as Config['automation']; config.automation.gcal ??= {} as NonNullable; config.automation.gcal.enabled = value; return true; }, 'automation.gdocs.enabled': (config, value) => { if (typeof value !== 'boolean') {return false;} config.automation ??= {} as Config['automation']; config.automation.gdocs ??= {} as NonNullable; config.automation.gdocs.enabled = value; return true; }, 'automation.gdrive.enabled': (config, value) => { if (typeof value !== 'boolean') {return false;} config.automation ??= {} as Config['automation']; config.automation.gdrive ??= {} as NonNullable; config.automation.gdrive.enabled = value; return true; }, 'automation.gtasks.enabled': (config, value) => { if (typeof value !== 'boolean') {return false;} config.automation ??= {} as Config['automation']; config.automation.gtasks ??= {} as NonNullable; config.automation.gtasks.enabled = value; return true; }, 'automation.heartbeat.enabled': (config, value) => { if (typeof value !== 'boolean') {return false;} config.automation ??= {} as Config['automation']; config.automation.heartbeat ??= {} as Config['automation']['heartbeat']; config.automation.heartbeat.enabled = value; return true; }, 'automation.heartbeat.interval': (config, value) => { if (typeof value !== 'string' || value.trim().length === 0) {return false;} config.automation ??= {} as Config['automation']; config.automation.heartbeat ??= {} as Config['automation']['heartbeat']; config.automation.heartbeat.interval = value.trim(); return true; }, 'automation.heartbeat.notify_cooldown': (config, value) => { if (typeof value !== 'string' || value.trim().length === 0) {return false;} config.automation ??= {} as Config['automation']; config.automation.heartbeat ??= {} as Config['automation']['heartbeat']; config.automation.heartbeat.notify_cooldown = value.trim(); return true; }, 'automation.heartbeat.failure_threshold': (config, value) => { if (typeof value !== 'number' || !Number.isFinite(value) || value < 1 || value > 10) {return false;} config.automation ??= {} as Config['automation']; config.automation.heartbeat ??= {} as Config['automation']['heartbeat']; config.automation.heartbeat.failure_threshold = Math.floor(value); return true; }, 'automation.heartbeat.disk_threshold_mb': (config, value) => { if (typeof value !== 'number' || !Number.isFinite(value) || value < 10) {return false;} config.automation ??= {} as Config['automation']; config.automation.heartbeat ??= {} as Config['automation']['heartbeat']; config.automation.heartbeat.disk_threshold_mb = Math.floor(value); return true; }, 'automation.heartbeat.process_memory_threshold_mb': (config, value) => { if (typeof value !== 'number' || !Number.isFinite(value) || value < 64) {return false;} config.automation ??= {} as Config['automation']; config.automation.heartbeat ??= {} as Config['automation']['heartbeat']; config.automation.heartbeat.process_memory_threshold_mb = Math.floor(value); return true; }, 'automation.heartbeat.backup_failure_threshold': (config, value) => { if (typeof value !== 'number' || !Number.isFinite(value) || value < 1 || value > 10) {return false;} config.automation ??= {} as Config['automation']; config.automation.heartbeat ??= {} as Config['automation']['heartbeat']; config.automation.heartbeat.backup_failure_threshold = Math.floor(value); return true; }, 'automation.heartbeat.provider_error_rate_threshold': (config, value) => { if (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > 1) {return false;} config.automation ??= {} as Config['automation']; config.automation.heartbeat ??= {} as Config['automation']['heartbeat']; config.automation.heartbeat.provider_error_rate_threshold = value; return true; }, 'automation.heartbeat.provider_error_min_calls': (config, value) => { if (typeof value !== 'number' || !Number.isFinite(value) || value < 1) {return false;} config.automation ??= {} as Config['automation']; config.automation.heartbeat ??= {} as Config['automation']['heartbeat']; config.automation.heartbeat.provider_error_min_calls = Math.floor(value); return true; }, 'automation.heartbeat.checks': (config, value) => { const allowed = ['gateway', 'model', 'channels', 'memory', 'disk', 'process_memory', 'backup', 'provider_errors']; if (!Array.isArray(value) || !value.every((entry) => typeof entry === 'string' && allowed.includes(entry))) { return false; } config.automation ??= {} as Config['automation']; config.automation.heartbeat ??= {} as Config['automation']['heartbeat']; config.automation.heartbeat.checks = value as Config['automation']['heartbeat']['checks']; return true; }, 'backup.enabled': (config, value) => { if (typeof value !== 'boolean') {return false;} config.backup ??= {} as Config['backup']; config.backup.enabled = value; return true; }, 'audio.enabled': (config, value) => { if (typeof value !== 'boolean') {return false;} config.audio ??= {} as Config['audio']; config.audio.enabled = value; return true; }, 'sandbox.enabled': (config, value) => { if (typeof value !== 'boolean') {return false;} config.sandbox ??= {} as Config['sandbox']; config.sandbox.enabled = value; return true; }, 'memory.daily_log.enabled': (config, value) => { if (typeof value !== 'boolean') {return false;} config.memory ??= {} as Config['memory']; config.memory.daily_log ??= {} as Config['memory']['daily_log']; config.memory.daily_log.enabled = value; return true; }, 'memory.proactive_extract.enabled': (config, value) => { if (typeof value !== 'boolean') {return false;} config.memory ??= {} as Config['memory']; config.memory.proactive_extract ??= {} as Config['memory']['proactive_extract']; config.memory.proactive_extract.enabled = value; return true; }, 'memory.proactive_extract.min_tool_calls': (config, value) => { if (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > 50) {return false;} config.memory ??= {} as Config['memory']; config.memory.proactive_extract ??= {} as Config['memory']['proactive_extract']; config.memory.proactive_extract.min_tool_calls = Math.floor(value); return true; }, 'tts.enabled': (config, value) => { if (typeof value !== 'boolean') {return false;} config.tts ??= {} as Config['tts']; config.tts.enabled = value; return true; }, 'tts.enabled_channels': (config, value) => { if (!Array.isArray(value) || !value.every((v) => typeof v === 'string' && v.trim().length > 0)) {return false;} config.tts ??= {} as Config['tts']; config.tts.enabled_channels = value as string[]; return true; }, }; export function createConfigHandlers(deps: ConfigHandlerDeps) { const syncModelRouterFromConfig = (nextConfig: Config): void => { const modelRouter = deps.modelRouter; if (!modelRouter) { return; } const updateTier = (tier: ModelTier, cfg: Config['models']['default'] | undefined) => { if (!cfg) { return; } try { const client = createClientFromConfig(cfg); modelRouter.setClient(tier, client, `${cfg.provider}/${cfg.model}`); modelRouter.setTierStrict(tier, false); } catch (error) { // Keep config.patch successful even when runtime credentials are unavailable for a selected provider. console.warn( `[gateway.config] Skipping runtime model router update for tier "${tier}": ${ error instanceof Error ? error.message : String(error) }`, ); } }; updateTier('default', nextConfig.models.default); updateTier('fast', nextConfig.models.fast); updateTier('complex', nextConfig.models.complex); updateTier('local', nextConfig.models.local); }; return { 'config.get': async (request: GatewayRequest): Promise => { return makeResponse(request.id, redactConfig(deps.config)); }, 'config.patch': async (request: GatewayRequest): Promise => { const patches = request.params?.patches; if (!patches || typeof patches !== 'object' || Array.isArray(patches)) { return makeError(request.id, ErrorCode.InvalidRequest, 'params.patches must be an object of { key: value } pairs'); } const applied: string[] = []; const rejected: string[] = []; const draft = JSON.parse(JSON.stringify(deps.config)) as Config; for (const [key, value] of Object.entries(patches as Record)) { const patcher = PATCHABLE_KEYS[key]; if (!patcher) { rejected.push(key); continue; } const ok = patcher(draft, value); if (ok) { applied.push(key); } else { rejected.push(key); } } if (applied.length === 0) { return makeResponse(request.id, { applied, rejected, persisted: false }); } if (deps.persistConfig) { try { await deps.persistConfig(draft); } catch (err) { return makeResponse(request.id, { applied: [], rejected, persisted: false, persistError: err instanceof Error ? err.message : String(err), }); } } // Update in-memory runtime config only after a successful persist (or when persistence is not configured). for (const key of Object.keys(deps.config)) { delete (deps.config as Record)[key]; } Object.assign(deps.config as Record, draft as Record); syncModelRouterFromConfig(draft); return makeResponse(request.id, { applied, rejected, persisted: Boolean(deps.persistConfig) }); }, }; }