main
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
# Архитектура 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 месяцев на реальных метриках.
|
||||
Reference in New Issue
Block a user