212 lines
13 KiB
Markdown
212 lines
13 KiB
Markdown
# Архитектура 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/<brain>, остальные работают |
|
||
| 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 месяцев на реальных метриках.
|