import type { GatewayRequest, OutboundMessage } from '../protocol.js'; import { makeResponse, makeError, ErrorCode } from '../protocol.js'; import type { Config } from '../../config/index.js'; export interface ConfigHandlerDeps { config: Config; } /** * Redact sensitive values from config before returning. * Replaces API keys, tokens, passwords, and other credentials with "***". * * Covers: telegram, discord, slack, 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'); // 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; }, }; export function createConfigHandlers(deps: ConfigHandlerDeps) { 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[] = []; for (const [key, value] of Object.entries(patches as Record)) { const patcher = PATCHABLE_KEYS[key]; if (!patcher) { rejected.push(key); continue; } const ok = patcher(deps.config, value); if (ok) { applied.push(key); } else { rejected.push(key); } } return makeResponse(request.id, { applied, rejected }); }, }; }