Files
ZBrain/docs/ARCHITECTURE.md
zuevav f4bca8449e main
2026-05-20 19:33:02 +03:00

212 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Архитектура 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 месяцев на реальных метриках.