# Архитектура ZBrain ## Обзор ZBrain — трёхслойная система: 1. **gbrain** (vendored) — движок: хранилище, эмбеддинги, гибридный поиск, MCP server 2. **brainhub** (наш код) — админка, прокси, auth, RBAC, audit 3. **PostgreSQL** — единая СУБД с изоляцией по БД на каждый брейн ## Принципы **Изоляция данных.** Каждый брейн (zetit, telerapharma, personal, community) живёт в отдельной БД с отдельным Postgres-пользователем. Утечка токена с правами на personal не даёт доступа к telerapharma на уровне СУБД. **Brainhub как единая точка входа.** Никакой Claude Code никогда не подключается к gbrain напрямую. Все MCP-запросы проходят через brainhub-proxy для аудита, rate limiting, проверки scope. **Vendored gbrain.** Зависимость от upstream'а закреплена в нашем git mirror'е с конкретным тегом. Обновления gbrain — управляемое решение, а не автоматический breaking change. **Two-zone deployment.** Внутренний контур (VPN, plain HTTP) для полного UI и админ-API; публичный контур (HTTPS + OAuth) только для `/mcp/*` и `/oauth/*`. ## Компонентная диаграмма ``` ┌─────────────────────────────────────────────────────────────────┐ │ Clients │ ├─────────────────────────────────────────────────────────────────┤ │ Claude Code (dev) Claude Code (prod) Cursor Browser │ │ │ │ │ │ │ └───────┼────────────────────┼──────────────────┼─────────┼───────┘ │ │ │ │ internal public via internal internal MCP nginx MCP HTTPS │ │ │ │ ▼ ▼ ▼ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Brainhub Process │ │ ┌─────────────────────┐ ┌────────────────────────────────────┐│ │ │ Internal Server │ │ Public Server ││ │ │ :3000 │ │ :3010 ││ │ │ - /api/* │ │ - /mcp/* (с auth) ││ │ │ - /mcp/* (с auth) │ │ - /oauth/* ││ │ │ - /api/events SSE │ │ ││ │ └──────────┬──────────┘ └──────────────┬─────────────────────┘│ │ │ │ │ │ └────────┬───────────────────┘ │ │ │ │ │ ┌──────────────────┴──────────────────┐ │ │ │ Shared modules │ │ │ │ - Auth service (passport.js) │ │ │ │ - MCP Proxy + Token validator │ │ │ │ - RBAC middleware │ │ │ │ - Audit logger │ │ │ │ - SSE event bus │ │ │ │ - Sync worker queue │ │ │ └──────────────────┬──────────────────┘ │ │ │ │ └──────────────────────┼──────────────────────────────────────────┘ │ ┌──────────────┴────────────┬─────────────┐ │ │ │ ▼ ▼ ▼ ┌───────────────┐ ┌────────────────┐ ┌──────────────┐ │ Postgres │ │ gbrain inst. │ │ CLI (exec) │ │ brainhub DB │ │ HTTP MCP │ │ │ │ - users │ │ 127.0.0.1: │ │ gbrain │ │ - sessions │ │ 3001..3099 │ │ init │ │ - tokens │ │ │ │ import │ │ - brains │ │ 21 MCP tools │ │ sync │ │ - sources │ │ │ │ │ │ - projects │ └────────┬───────┘ └──────┬───────┘ │ - audit_log │ │ │ └───────────────┘ ▼ ▼ ┌─────────────────────────────────┐ │ Postgres (per-brain DBs) │ │ - gbrain_zetit │ │ - gbrain_telerapharma │ │ - gbrain_personal │ │ - gbrain_community │ │ │ │ Каждая со своим owner-user'ом │ └─────────────────────────────────┘ ``` ## Flow: MCP-запрос от Claude Code Допустим, Claude Code на сервере клиента делает search-запрос: ``` 1. Claude Code: POST https://brain.zetit.ru/mcp/zetit Authorization: Bearer brain_pat_a8f3b2c1... Content-Type: application/json Body: {"jsonrpc":"2.0", "method":"tools/call", "params":{"name":"query","arguments":{"query":"how to renew sstp cert"}}} 2. Nginx (brain.zetit.ru): - rate_limit per IP (30 r/s) и per token (100 r/s) - проксирует на 127.0.0.1:3010 3. Brainhub public server: - MCPProxy.middleware валидирует токен - извлекает token из Bearer, считает SHA-256 - смотрит в кэш (in-memory, TTL 30s) - если нет в кэше: SELECT FROM access_tokens WHERE token_hash = ? - проверки: revoked_at IS NULL, expires_at > now(), IP в allowlist - проверяет scope: содержит ли scopes[] 'mcp:read:zetit' или 'mcp:write:zetit' - резолвит брейн: SELECT mcp_internal_port FROM brains WHERE name = 'zetit' - пишет в audit_log queue: {action: 'mcp.call', resource: 'zetit', token_id: ..., method: 'query'} - throttled update: token.last_used_at = now() (не чаще раза в минуту) - проксирует на http://127.0.0.1:3001/mcp 4. gbrain (zetit instance): - выполняет hybrid search - возвращает результаты 5. Brainhub возвращает Claude Code stream of bytes как есть ``` **Времена:** - Без кэша: ~3-5ms (DB lookup) + gbrain латенси - С кэшем: ~0.5ms (cache hit) + gbrain латенси - Audit batch insert: раз в секунду, async, не влияет на латенси ## Структура процесса brainhub Один Node.js процесс с двумя Express apps на разных портах: ```typescript // apps/api/src/main.ts const internalApp = createInternalApp(); // /api, /mcp (с auth), UI proxy const publicApp = createPublicApp(); // /mcp, /oauth, /health internalApp.listen(3000, '127.0.0.1'); // только localhost publicApp.listen(3010, '127.0.0.1'); // только localhost ``` Nginx разруливает между ними по hostname: - `brain.zetit.local` → 127.0.0.1:3000 - `brain.zetit.ru` → 127.0.0.1:3010 Это даёт shared codebase, но чёткое разделение endpoints. Невозможно случайно выставить admin API в публичный контур. ## Sync worker Sync — долгая операция (минуты до часов для большого корпуса). Не делать в-process на Express запросе. ```typescript // Используем BullMQ или нативный setInterval с in-memory очередью // API endpoint только ставит задачу в очередь POST /api/brains/zetit/sources/git-1/sync → syncQueue.add('sync', {brainId, sourceId}) → return {jobId} // Worker (отдельный процесс или setInterval): syncQueue.process('sync', async (job) => { const {brainId, sourceId} = job.data; const source = await db.sources.findById(sourceId); // git pull await exec(`cd ${source.config.local_path} && git pull`); // запуск gbrain через CLI const process = spawn('gbrain', ['sync', '--source', source.config.local_path], { env: {...process.env, DATABASE_URL: brain.database_url} }); // streaming output через SSE process.stdout.on('data', (chunk) => { sseEventBus.publish(`brain:${brainId}:sync:${sourceId}`, chunk.toString()); }); // update source.last_sync_at }); ``` Cron триггерится из того же worker по расписанию (`source.schedule = '0 */6 * * *'`). ## Кэширование | Что | Где | TTL | Зачем | |-----|-----|-----|-------| | Token validation | Memory (LRU) | 30s | избежать DB hit на каждом MCP запросе | | Brain port resolution | Memory | 5min | редко меняется | | User session | Memory | 5min | избежать DB hit на каждом auth check | | OAuth state | Redis (опционально) | 10min | CSRF protection для OAuth flow | В первой версии всё in-memory. Если процесс будет масштабироваться горизонтально — выносим в Redis. ## Failure modes и их обработка | Что ломается | Что происходит | Что делаем | |--------------|----------------|------------| | Postgres down | brainhub не отвечает | health check 503, alerts | | gbrain instance crash | конкретный брейн не отвечает | brainhub возвращает 502 на /mcp/, остальные работают | | OpenAI API down | sync падает, search работает (без re-embed) | retry с backoff в worker | | FNA proxy down | gbrain не может делать embed | sync queue ждёт, alerts | | Disk full | Postgres перестаёт писать | Prometheus alert заранее на 80% | | Token leaked | злоумышленник делает запросы | Revoke в UI → cache invalidate в течение 30s | ## Что НЕ делаем в первой версии Сознательно откладываем, чтобы не утонуть: - Horizontal scaling brainhub (один процесс на одной VM достаточно для нашего масштаба) - Sharding по брейнам на разные хосты (всё в одном Postgres) - Многотенантность (только наши пользователи, не SaaS) - WebSockets (SSE достаточно для realtime UI) - Service mesh, k8s (overkill для одной VM) - Свой forked gbrain (используем upstream через mirror) Эти решения можно пересмотреть через 6-12 месяцев на реальных метриках.