13 KiB
Архитектура ZBrain
Обзор
ZBrain — трёхслойная система:
- gbrain (vendored) — движок: хранилище, эмбеддинги, гибридный поиск, MCP server
- brainhub (наш код) — админка, прокси, auth, RBAC, audit
- 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 на разных портах:
// 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:3000brain.zetit.ru→ 127.0.0.1:3010
Это даёт shared codebase, но чёткое разделение endpoints. Невозможно случайно выставить admin API в публичный контур.
Sync worker
Sync — долгая операция (минуты до часов для большого корпуса). Не делать в-process на Express запросе.
// Используем 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 месяцев на реальных метриках.