9be8f76bc7
- Lane Queue: per-session FIFO queue in gateway replacing reject-when-busy (9 tests) - Credential Redaction: redactConfig() expanded to cover 18+ secret fields (16 tests) - Web UI Token Dashboard: system.tokenUsage endpoint + Usage page with summary cards - xAI (Grok) Provider: OpenAI-compatible client with model pricing - Voyage AI Embeddings: new embedding provider with configurable dimensions (5 tests) - Update gap analysis: 90→95 match (70%→74%), Tier 3 section marked DONE - Update state.json: test count 1001→1034, add tier3_completion entry Total: 1034 tests passing across 85 files, typecheck clean
159 lines
5.4 KiB
TypeScript
159 lines
5.4 KiB
TypeScript
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<string, unknown> {
|
|
const raw = JSON.parse(JSON.stringify(config)) as Record<string, unknown>;
|
|
|
|
// Helper: redact specified keys on an object if they exist and are non-nullish
|
|
const redact = (obj: Record<string, unknown> | 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<string, unknown>, 'bot_token');
|
|
|
|
// Discord
|
|
redact(raw.discord as Record<string, unknown>, 'bot_token');
|
|
|
|
// Slack
|
|
redact(raw.slack as Record<string, unknown>, 'bot_token', 'app_token', 'signing_secret');
|
|
|
|
// Server (gateway bearer token)
|
|
redact(raw.server as Record<string, unknown>, 'token');
|
|
|
|
// Models — tiers, their fallbacks, and local_providers (+ their fallbacks)
|
|
const models = raw.models as Record<string, unknown> | undefined;
|
|
if (models) {
|
|
for (const tier of ['default', 'fast', 'complex', 'local']) {
|
|
const m = models[tier] as Record<string, unknown> | undefined;
|
|
redact(m, 'api_key', 'auth_token');
|
|
const fb = m?.fallback as Record<string, unknown> | undefined;
|
|
redact(fb, 'api_key', 'auth_token');
|
|
}
|
|
const localProviders = models.local_providers as Record<string, Record<string, unknown>> | undefined;
|
|
if (localProviders) {
|
|
for (const provider of Object.values(localProviders)) {
|
|
redact(provider, 'api_key', 'auth_token');
|
|
const fb = provider.fallback as Record<string, unknown> | undefined;
|
|
redact(fb, 'api_key', 'auth_token');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Web search
|
|
redact(raw.web_search as Record<string, unknown>, 'api_key');
|
|
|
|
// Audio
|
|
redact(raw.audio as Record<string, unknown>, 'transcription_api_key');
|
|
|
|
// Memory → embedding
|
|
const memory = raw.memory as Record<string, unknown> | undefined;
|
|
if (memory) {
|
|
redact(memory.embedding as Record<string, unknown>, 'api_key');
|
|
}
|
|
|
|
// Automation — webhook HMAC secrets and gmail credential paths
|
|
const automation = raw.automation as Record<string, unknown> | undefined;
|
|
if (automation) {
|
|
const webhooks = automation.webhooks as Record<string, unknown>[] | undefined;
|
|
if (webhooks) {
|
|
for (const wh of webhooks) {
|
|
redact(wh, 'secret');
|
|
}
|
|
}
|
|
const gmail = automation.gmail as Record<string, unknown> | undefined;
|
|
redact(gmail, 'credentials_file', 'token_file');
|
|
}
|
|
|
|
// MCP server env vars (may contain API keys or other secrets)
|
|
const mcp = raw.mcp as Record<string, unknown> | undefined;
|
|
if (mcp) {
|
|
const servers = mcp.servers as Record<string, unknown>[] | undefined;
|
|
if (servers) {
|
|
for (const srv of servers) {
|
|
if (srv.env && typeof srv.env === 'object') {
|
|
const env = srv.env as Record<string, unknown>;
|
|
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<string, (config: Config, value: unknown) => 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<OutboundMessage> => {
|
|
return makeResponse(request.id, redactConfig(deps.config));
|
|
},
|
|
|
|
'config.patch': async (request: GatewayRequest): Promise<OutboundMessage> => {
|
|
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<string, unknown>)) {
|
|
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 });
|
|
},
|
|
};
|
|
}
|