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 месяцев на реальных метриках.
|
||||
@@ -0,0 +1,60 @@
|
||||
# ADR-0001: Vendored gbrain через git mirror
|
||||
|
||||
**Дата:** 2026-05-20
|
||||
**Статус:** Принято
|
||||
|
||||
## Контекст
|
||||
|
||||
gbrain (https://github.com/garrytan/gbrain) — активно развивающийся проект:
|
||||
- 60+ коммитов за последние 2 месяца
|
||||
- v0.26 (текущая) ввела breaking change: переход с bearer tokens на OAuth 2.1
|
||||
- Public API субпакетов (`gbrain/operations`, `gbrain/pglite-engine`) меняется
|
||||
|
||||
Дополнительные факторы:
|
||||
- GitHub аккаунт zuevav был ограничен в апреле 2026, доступ к публичным репозиториям через `bun install -g github:garrytan/gbrain` может неожиданно ломаться
|
||||
- ZBrain зависит от стабильности gbrain в production
|
||||
- При следующей блокировке аккаунта мы не сможем переустановить или обновить gbrain
|
||||
|
||||
## Решение
|
||||
|
||||
Не зависеть от GitHub в runtime/install path. Зеркалить gbrain в свой git:
|
||||
|
||||
```bash
|
||||
# Однократно, на чистом ноуте с работающим GitHub доступом:
|
||||
git clone --mirror https://github.com/garrytan/gbrain.git
|
||||
cd gbrain.git
|
||||
git remote set-url --push origin git@git.zetit.ru:zuevav/gbrain-mirror.git
|
||||
git push --mirror
|
||||
```
|
||||
|
||||
Bootstrap-скрипт клонит из git.zetit.ru:
|
||||
|
||||
```bash
|
||||
GBRAIN_REPO="https://git.zetit.ru/zuevav/gbrain-mirror.git" \
|
||||
GBRAIN_VERSION="v0.26.5" \
|
||||
bash scripts/bootstrap-vm.sh
|
||||
```
|
||||
|
||||
## Последствия
|
||||
|
||||
### Плюсы
|
||||
- Независимость от GitHub доступности и блокировок аккаунтов
|
||||
- Управляемое обновление gbrain (через `git fetch upstream && review && push to mirror`)
|
||||
- Возможность hotfix'ов локально, если upstream сломан (как форк, но в общем git)
|
||||
- Защита от supply chain атак (мы видим diff перед обновлением)
|
||||
|
||||
### Минусы
|
||||
- Нужно вручную обновлять mirror (раз в месяц-два)
|
||||
- Лишний шаг в setup
|
||||
- Расхождение с upstream если хотфикс не запушим обратно
|
||||
|
||||
### Митигация
|
||||
- Раз в месяц: проверка upstream'а, review diff, push to mirror
|
||||
- Все наши изменения (если потребуются) — в отдельной ветке `zetit/*`, чтобы не мерджились с upstream
|
||||
- Tag'ируем версии в нашем mirror'е (`v0.26.5-zetit-1`, если есть наши патчи)
|
||||
|
||||
## Альтернативы рассмотренные
|
||||
|
||||
1. **Прямая зависимость от github:garrytan/gbrain** — отвергнуто из-за рисков блокировки и breaking changes
|
||||
2. **Свой fork на git.zetit.ru с активной разработкой** — overkill, мы не планируем существенно дорабатывать gbrain
|
||||
3. **npm-publish gbrain в свой private npm registry** — gbrain не публикуется в npm как пакет; нужен install via bun + workspace
|
||||
@@ -0,0 +1,92 @@
|
||||
# ADR-0002: Two-zone deployment (внутренний + публичный контур)
|
||||
|
||||
**Дата:** 2026-05-20
|
||||
**Статус:** Принято
|
||||
|
||||
## Контекст
|
||||
|
||||
ZBrain должен обслуживать:
|
||||
|
||||
1. **Внутренних пользователей** — owner (Алексей) и будущая команда работают через VPN/локальную сеть ZETIT. Им нужен полный UI: дашборд, управление брейнами, токенами, источниками, аудит.
|
||||
|
||||
2. **Внешних агентов** — Claude Code на серверах клиентов (TeleraPharma, Fontvielle, и т.д.), которым нужен MCP-доступ к чтению/записи брейнов. У этих серверов нет VPN-доступа к ZETIT.
|
||||
|
||||
Простое решение "выставить всё на публичный домен с auth" имеет недостатки:
|
||||
- Любая уязвимость в админ-UI становится публично доступной
|
||||
- OAuth flow на админ-эндпоинтах усложняет admin интеграции (curl, скрипты)
|
||||
- Утечка session cookie с админ-привилегиями через CSRF/XSS критична
|
||||
|
||||
## Решение
|
||||
|
||||
Развести функционал по двум сетевым контурам с разными доменами:
|
||||
|
||||
### Внутренний контур (brain.zetit.local)
|
||||
|
||||
- Доступен **только** из сети ZETIT и через VPN
|
||||
- Plain HTTP допустим (доверенная сеть, нет MITM риска)
|
||||
- Endpoints: **всё** — `/`, `/api/*`, `/mcp/*`, `/api/events` (SSE)
|
||||
- Auth: session cookie через local login (email/password) или OAuth
|
||||
- Используется: для web-UI, админских скриптов, debug'а
|
||||
|
||||
### Публичный контур (brain.zetit.ru)
|
||||
|
||||
- Доступен из интернета через TLS
|
||||
- Endpoints: **только** `/mcp/*`, `/oauth/*`, `/health`
|
||||
- Auth: Bearer tokens (для MCP) или OAuth flow (для подключения нового клиента)
|
||||
- Любой другой path → 404 (без подсказок что вообще существует)
|
||||
- Rate limiting per IP и per token
|
||||
- Используется: для удалённого Claude Code/Cursor, для машинного доступа
|
||||
|
||||
## Реализация
|
||||
|
||||
Один Node.js процесс запускает два независимых Express app на разных портах:
|
||||
|
||||
```typescript
|
||||
// apps/api/src/main.ts
|
||||
const internalApp = createInternalApp(); // полный набор routes
|
||||
const publicApp = createPublicApp(); // только /mcp и /oauth
|
||||
|
||||
internalApp.listen(3000, '127.0.0.1');
|
||||
publicApp.listen(3010, '127.0.0.1');
|
||||
```
|
||||
|
||||
Nginx разруливает по hostname:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
server_name brain.zetit.local;
|
||||
location / { proxy_pass http://127.0.0.1:3000; }
|
||||
}
|
||||
|
||||
server {
|
||||
server_name brain.zetit.ru;
|
||||
listen 443 ssl;
|
||||
location /mcp/ { proxy_pass http://127.0.0.1:3010; }
|
||||
location /oauth/ { proxy_pass http://127.0.0.1:3010; }
|
||||
location / { return 404; }
|
||||
}
|
||||
```
|
||||
|
||||
## Последствия
|
||||
|
||||
### Плюсы
|
||||
- Атака на админ-UI требует VPN или физического доступа к сети
|
||||
- Невозможно случайно выставить админ-API в публичный контур (architectural enforcement)
|
||||
- Rate limit на публичном контуре более агрессивный, не мешает админам
|
||||
- Раздельные access логи nginx для security audit
|
||||
|
||||
### Минусы
|
||||
- Сложнее тестировать: нужно эмулировать два контура локально
|
||||
- DNS: две A-записи на разных серверах (внутренний и edge)
|
||||
- Невозможно "быстро дать админский доступ" партнёру не давая VPN
|
||||
|
||||
### Митигация для разработки
|
||||
- Локально оба app слушают `127.0.0.1` на разных портах, host header проверяется в middleware
|
||||
- `.env.development` имеет флаг `DISABLE_ZONE_CHECK=true` для упрощённого dev режима
|
||||
- Integration тесты гоняют оба контура
|
||||
|
||||
## Альтернативы рассмотренные
|
||||
|
||||
1. **Один контур с строгой ролевой моделью** — отвергнуто, ошибка в RBAC становится критичной уязвимостью
|
||||
2. **Два разных процесса/контейнера** — overkill для одной VM, осложняет shared state (token cache)
|
||||
3. **API Gateway (Kong/Traefik)** — лишняя сущность для проекта такого размера
|
||||
@@ -0,0 +1,103 @@
|
||||
# ADR-0003: Изоляция брейнов через отдельные PostgreSQL БД
|
||||
|
||||
**Дата:** 2026-05-20
|
||||
**Статус:** Принято
|
||||
|
||||
## Контекст
|
||||
|
||||
ZBrain хостит несколько брейнов с разным уровнем чувствительности данных:
|
||||
|
||||
- **zetit** — внутренние процессы ZETIT, не публичные
|
||||
- **telerapharma** — корпоративная инфраструктура с NDA
|
||||
- **personal** — личные данные владельца
|
||||
- **community** — данные о Смоленской 10, дом-чат, ЖКХ
|
||||
|
||||
Утечка MCP токена с правами на один брейн **не должна** давать доступ к другим брейнам.
|
||||
|
||||
Варианты изоляции в Postgres (от слабой к сильной):
|
||||
|
||||
1. **Одна БД, одна схема, отдельные таблицы с префиксами** — слабая изоляция, любой SQL injection даёт доступ ко всему
|
||||
2. **Одна БД, отдельные schema'ы (zetit.pages, telerapharma.pages)** — изоляция через `search_path`, но один Postgres user видит всё
|
||||
3. **Одна БД, отдельные пользователи с row-level security** — сложно, легко допустить ошибку в RLS политиках
|
||||
4. **Отдельные БД с отдельными пользователями** — изоляция на уровне СУБД
|
||||
|
||||
## Решение
|
||||
|
||||
Вариант 4: **отдельная БД + отдельный Postgres-пользователь на каждый брейн**.
|
||||
|
||||
```
|
||||
PostgreSQL cluster
|
||||
├── brainhub (БД)
|
||||
│ └── владелец: brainhub
|
||||
│ └── таблицы: users, sessions, tokens, brains, sources, projects, audit_log
|
||||
│
|
||||
├── gbrain_zetit (БД)
|
||||
│ └── владелец: gbrain_zetit
|
||||
│ └── полная схема gbrain (pages, chunks, embeddings, links, ...)
|
||||
│
|
||||
├── gbrain_telerapharma (БД)
|
||||
│ └── владелец: gbrain_telerapharma
|
||||
│ └── ...
|
||||
│
|
||||
├── gbrain_personal (БД)
|
||||
│ └── владелец: gbrain_personal
|
||||
│ └── ...
|
||||
│
|
||||
└── gbrain_community (БД)
|
||||
└── владелец: gbrain_community
|
||||
└── ...
|
||||
```
|
||||
|
||||
Каждый gbrain-instance подключается к Postgres под своим пользователем и **физически не видит** другие БД:
|
||||
|
||||
```sql
|
||||
-- Под пользователем gbrain_zetit
|
||||
\l
|
||||
-- Покажет только gbrain_zetit (и template/postgres)
|
||||
|
||||
\c gbrain_telerapharma
|
||||
-- ERROR: permission denied for database gbrain_telerapharma
|
||||
```
|
||||
|
||||
Brainhub использует свою БД `brainhub` под пользователем `brainhub` (тоже не видит остальные).
|
||||
|
||||
## Реализация
|
||||
|
||||
В `scripts/create-brain.sh`:
|
||||
|
||||
```sql
|
||||
CREATE USER gbrain_${BRAIN_NAME} WITH PASSWORD '${random_password}';
|
||||
CREATE DATABASE gbrain_${BRAIN_NAME} OWNER gbrain_${BRAIN_NAME};
|
||||
GRANT ALL PRIVILEGES ON DATABASE gbrain_${BRAIN_NAME} TO gbrain_${BRAIN_NAME};
|
||||
-- НЕ выдаём этому юзеру ничего на other databases
|
||||
```
|
||||
|
||||
Пароль для каждого gbrain-юзера генерируется случайно и хранится:
|
||||
1. В `/var/lib/zbrain/brains/<name>/config.json` (chmod 600, owner zbrain)
|
||||
2. В DATABASE_URL внутри systemd unit (Environment="DATABASE_URL=...")
|
||||
|
||||
`brainhub` НЕ хранит эти пароли в своей БД (они не нужны для admin-операций) — только знает на каком порту слушает gbrain instance.
|
||||
|
||||
## Последствия
|
||||
|
||||
### Плюсы
|
||||
- Физическая изоляция через ACL Postgres
|
||||
- SQL injection в одном брейне не даёт доступ к другим
|
||||
- pg_dump каждого брейна тривиально по отдельности
|
||||
- Удаление брейна = одна команда `DROP DATABASE gbrain_X`
|
||||
- Разные параметры тюнинга на разные брейны (если потребуется)
|
||||
|
||||
### Минусы
|
||||
- Нельзя сделать JOIN между брейнами в SQL (но это и не нужно — поиск через brainhub)
|
||||
- Каждая БД имеет свои индексы, общая нагрузка на shared_buffers Postgres делится
|
||||
- Backup делает дамп каждой БД отдельно (несложно)
|
||||
|
||||
### Митигация
|
||||
- shared_buffers задан с запасом (2GB на 8GB RAM)
|
||||
- HNSW индексы строятся в `maintenance_work_mem=512MB`, одного брейна за раз достаточно
|
||||
|
||||
## Альтернативы рассмотренные
|
||||
|
||||
1. **Row-level security в одной БД** — сложно, требует консистентного применения политик на каждой таблице gbrain (а у gbrain их десятки). Один пропущенный CREATE POLICY = утечка
|
||||
2. **Отдельные Postgres-кластеры на каждый брейн** — overkill, 4× процессов, 4× shared_buffers
|
||||
3. **Отдельные VM на каждый брейн** — экстремальная изоляция, не нужна на нашем масштабе
|
||||
@@ -0,0 +1,362 @@
|
||||
# Deployment Guide
|
||||
|
||||
Полная инструкция по развёртыванию ZBrain с нуля.
|
||||
|
||||
## Стадия 1: Подготовка VM
|
||||
|
||||
### Спецификация
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| ОС | Ubuntu 22.04 LTS Server |
|
||||
| CPU | 4 vCPU (минимум 2) |
|
||||
| RAM | 8 GB (минимум 4) |
|
||||
| Disk | 80 GB SSD (минимум 40) |
|
||||
| FS | ext4 |
|
||||
| Network | статический IP в сети ZETIT |
|
||||
|
||||
### Создание
|
||||
|
||||
1. На гипервизоре создать VM с указанными параметрами
|
||||
2. Установить Ubuntu 22.04 LTS Server (minimal)
|
||||
3. Создать пользователя `admin` с sudo правами
|
||||
4. Скопировать SSH-ключи: `ssh-copy-id admin@<ip>`
|
||||
5. Отключить password auth: `sudo passwd -l root` и в `/etc/ssh/sshd_config` `PasswordAuthentication no`
|
||||
|
||||
### Базовая конфигурация
|
||||
|
||||
```bash
|
||||
# Hostname
|
||||
sudo hostnamectl set-hostname zbrain
|
||||
echo "127.0.1.1 zbrain.zetit.local zbrain" | sudo tee -a /etc/hosts
|
||||
|
||||
# Часовой пояс
|
||||
sudo timedatectl set-timezone Europe/Moscow
|
||||
|
||||
# Обновления
|
||||
sudo apt-get update && sudo apt-get upgrade -y
|
||||
sudo apt-get install -y unattended-upgrades
|
||||
```
|
||||
|
||||
### DNS
|
||||
|
||||
В твоей внутренней DNS зоне (zetit.local или эквивалент):
|
||||
|
||||
```
|
||||
brain.zetit.local. A <internal-ip>
|
||||
```
|
||||
|
||||
На внешнем DNS (если используешь публичный контур):
|
||||
|
||||
```
|
||||
brain.zetit.ru. A <external-ip-of-edge-server>
|
||||
```
|
||||
|
||||
Edge-сервер (где живёт nginx с публичным контуром) проксирует на внутренний IP VM.
|
||||
|
||||
## Стадия 2: Bootstrap
|
||||
|
||||
```bash
|
||||
# Склонируй ZBrain repo
|
||||
sudo mkdir -p /opt/zbrain && sudo chown $USER:$USER /opt/zbrain
|
||||
git clone git@git.zetit.ru:zuevav/ZBrain.git /opt/zbrain
|
||||
cd /opt/zbrain
|
||||
|
||||
# Зеркало gbrain (один раз, не на каждой VM)
|
||||
# Это делается единожды на твоей рабочей машине:
|
||||
#
|
||||
# git clone --mirror https://github.com/garrytan/gbrain.git
|
||||
# cd gbrain.git
|
||||
# git push --mirror git@git.zetit.ru:zuevav/gbrain-mirror.git
|
||||
|
||||
# Запусти bootstrap
|
||||
sudo TOTAL_RAM_GB=8 FNA_PROXY="http://client_001:PASSWORD@fna.zetit.ru:3128" bash scripts/bootstrap-vm.sh
|
||||
```
|
||||
|
||||
После завершения скрипт покажет следующие шаги.
|
||||
|
||||
## Стадия 3: Postgres secrets
|
||||
|
||||
```bash
|
||||
sudo -u postgres psql
|
||||
```
|
||||
|
||||
```sql
|
||||
-- Пароль root postgres юзера (сохрани в KeePass)
|
||||
ALTER USER postgres PASSWORD 'STRONG_RANDOM_PASSWORD_24_CHARS';
|
||||
|
||||
-- Пользователь для brainhub
|
||||
CREATE USER brainhub WITH PASSWORD 'ANOTHER_STRONG_PASSWORD';
|
||||
GRANT ALL PRIVILEGES ON DATABASE brainhub TO brainhub;
|
||||
|
||||
-- Проверка
|
||||
\du
|
||||
\l
|
||||
|
||||
\q
|
||||
```
|
||||
|
||||
## Стадия 4: Заполнение .env
|
||||
|
||||
```bash
|
||||
sudo cp /etc/zbrain/env.example /etc/zbrain/.env
|
||||
sudo chmod 600 /etc/zbrain/.env
|
||||
sudo chown zbrain:zbrain /etc/zbrain/.env
|
||||
sudo nano /etc/zbrain/.env
|
||||
```
|
||||
|
||||
Сгенерируй секреты:
|
||||
|
||||
```bash
|
||||
# SESSION_SECRET
|
||||
openssl rand -hex 32
|
||||
|
||||
# JWT_SECRET
|
||||
openssl rand -hex 32
|
||||
|
||||
# TOKEN_ENCRYPTION_KEY (для шифрования TOTP секретов)
|
||||
openssl rand -hex 16
|
||||
```
|
||||
|
||||
Заполни:
|
||||
- `DATABASE_URL` — с паролем brainhub
|
||||
- Секреты выше
|
||||
- `OPENAI_API_KEY` и `ANTHROPIC_API_KEY`
|
||||
- FNA-прокси с реальными credentials
|
||||
- `INITIAL_OWNER_EMAIL` и `INITIAL_OWNER_PASSWORD` (временный, сменишь после первого логина)
|
||||
|
||||
## Стадия 5: Первый брейн
|
||||
|
||||
```bash
|
||||
sudo bash /opt/zbrain/scripts/create-brain.sh zetit "ZETIT MSP" 3001
|
||||
```
|
||||
|
||||
Скрипт:
|
||||
1. Создаст пользователя `gbrain_zetit` и БД `gbrain_zetit` в Postgres
|
||||
2. Положит конфиг в `/var/lib/zbrain/brains/zetit/`
|
||||
3. Запустит миграции gbrain
|
||||
4. Создаст и запустит systemd unit `zbrain-gbrain-zetit`
|
||||
|
||||
Проверка:
|
||||
|
||||
```bash
|
||||
systemctl status zbrain-gbrain-zetit
|
||||
journalctl -u zbrain-gbrain-zetit -f # в отдельном окне
|
||||
curl http://localhost:3001/health
|
||||
```
|
||||
|
||||
## Стадия 6: Импорт первых данных
|
||||
|
||||
```bash
|
||||
sudo -u zbrain bash -c '
|
||||
export PATH=$HOME/.bun/bin:$PATH
|
||||
source /etc/zbrain/.env
|
||||
cd /var/lib/zbrain/gbrain
|
||||
bun run gbrain import /path/to/your/markdown/files
|
||||
'
|
||||
```
|
||||
|
||||
Проверка:
|
||||
|
||||
```bash
|
||||
sudo -u postgres psql -d gbrain_zetit -c '
|
||||
SELECT
|
||||
(SELECT count(*) FROM pages) AS pages,
|
||||
(SELECT count(*) FROM content_chunks) AS chunks,
|
||||
(SELECT count(*) FROM content_chunks WHERE embedding IS NOT NULL) AS embedded;
|
||||
'
|
||||
```
|
||||
|
||||
## Стадия 7: Brainhub API + Web
|
||||
|
||||
> Эта часть требует, чтобы код Sprint 2-5 был написан. До этого пропускаем.
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
cd /opt/zbrain
|
||||
sudo -u zbrain bun install
|
||||
sudo -u zbrain bun run build
|
||||
```
|
||||
|
||||
### Миграции
|
||||
|
||||
```bash
|
||||
sudo -u zbrain bun run --cwd apps/api db:migrate
|
||||
```
|
||||
|
||||
### Запуск через docker-compose (рекомендуется в prod)
|
||||
|
||||
```bash
|
||||
cd /opt/zbrain/deploy/docker
|
||||
sudo docker compose up -d
|
||||
sudo docker compose ps
|
||||
```
|
||||
|
||||
### Альтернатива: systemd
|
||||
|
||||
Если не хочешь docker - использовать `deploy/systemd/zbrain-brainhub.service`.
|
||||
|
||||
## Стадия 8: Nginx (внутренний контур)
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y nginx
|
||||
|
||||
# Скопируй конфиг
|
||||
sudo cp /opt/zbrain/deploy/nginx/zbrain.conf /etc/nginx/sites-available/zbrain
|
||||
|
||||
# Закомментируй временно public server-блок (TLS пока нет)
|
||||
|
||||
sudo ln -s /etc/nginx/sites-available/zbrain /etc/nginx/sites-enabled/
|
||||
sudo rm /etc/nginx/sites-enabled/default
|
||||
|
||||
# Проверь
|
||||
sudo nginx -t
|
||||
|
||||
# Запусти
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
Тест:
|
||||
|
||||
```bash
|
||||
curl -H "Host: brain.zetit.local" http://localhost/
|
||||
```
|
||||
|
||||
## Стадия 9: TLS для публичного контура
|
||||
|
||||
> Только если у тебя есть edge-сервер с внешним IP и DNS-запись brain.zetit.ru указывает на него.
|
||||
|
||||
На edge-сервере (или на самой VM, если она имеет внешний IP):
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y certbot python3-certbot-nginx
|
||||
sudo certbot --nginx -d brain.zetit.ru
|
||||
```
|
||||
|
||||
Раскомментируй public server-блок в nginx и перезагрузи.
|
||||
|
||||
## Стадия 10: Первый логин
|
||||
|
||||
1. Открой http://brain.zetit.local в браузере
|
||||
2. Залогинься как `INITIAL_OWNER_EMAIL` / `INITIAL_OWNER_PASSWORD`
|
||||
3. **Сразу смени пароль** в Profile
|
||||
4. Включи 2FA (когда Sprint 7 готов)
|
||||
5. Удали `INITIAL_OWNER_*` из `.env` (необязательно, но гигиенично)
|
||||
|
||||
## Стадия 11: Подключение Claude Code
|
||||
|
||||
1. В UI: создай токен с scope `mcp:write:zetit`
|
||||
2. Скопируй токен
|
||||
3. На своей рабочей машине добавь в `~/.claude/mcp.json` или в `.claude/mcp.json` проекта:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"zbrain-zetit": {
|
||||
"transport": "http",
|
||||
"url": "http://brain.zetit.local/mcp/zetit",
|
||||
"headers": {
|
||||
"Authorization": "Bearer brain_pat_<your-token>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. Перезапусти Claude Code
|
||||
5. Проверь что MCP-инструменты доступны
|
||||
|
||||
## Стадия 12: Backup
|
||||
|
||||
```bash
|
||||
# Создай age ключ для шифрования бэкапов (только один раз!)
|
||||
age-keygen -o /tmp/backup.age
|
||||
cat /tmp/backup.age # сохрани оба ключа в KeePass!
|
||||
|
||||
# Положи публичную часть на VM
|
||||
sudo head -1 /tmp/backup.age | grep public | awk '{print $NF}' > /tmp/pub
|
||||
sudo mv /tmp/pub /etc/zbrain/backup.age.pub
|
||||
sudo chmod 644 /etc/zbrain/backup.age.pub
|
||||
|
||||
# Удали /tmp/backup.age с VM!
|
||||
shred -u /tmp/backup.age
|
||||
|
||||
# Тест бэкапа
|
||||
sudo bash /opt/zbrain/scripts/backup.sh
|
||||
|
||||
# Добавь в cron
|
||||
sudo crontab -e
|
||||
# Добавь строку:
|
||||
# 0 3 * * * /opt/zbrain/scripts/backup.sh >> /var/log/zbrain/backup.log 2>&1
|
||||
```
|
||||
|
||||
## Чек-лист готовности
|
||||
|
||||
- [ ] VM создана с правильными параметрами
|
||||
- [ ] Bootstrap отработал без ошибок
|
||||
- [ ] Postgres работает, пароли установлены
|
||||
- [ ] `.env` заполнен и защищён 600
|
||||
- [ ] Первый брейн создан и работает
|
||||
- [ ] Данные импортированы
|
||||
- [ ] Brainhub запущен и доступен через nginx
|
||||
- [ ] OAuth настроен (если нужно)
|
||||
- [ ] TLS работает (если публичный контур)
|
||||
- [ ] Owner логин работает, пароль сменён
|
||||
- [ ] Claude Code успешно подключается через токен
|
||||
- [ ] Backup делается и шифруется
|
||||
- [ ] Восстановление из бэкапа протестировано (на отдельной VM)
|
||||
- [ ] Мониторинг настроен (опционально)
|
||||
- [ ] Документация в `docs/INFRASTRUCTURE.md` обновлена
|
||||
|
||||
## Типовые проблемы
|
||||
|
||||
### gbrain не стартует
|
||||
|
||||
```bash
|
||||
journalctl -u zbrain-gbrain-zetit -n 50 --no-pager
|
||||
```
|
||||
|
||||
Частые причины:
|
||||
- Пароль в DATABASE_URL не совпадает
|
||||
- FNA-прокси недоступен
|
||||
- Bun не в PATH (проверь Environment="PATH=..." в unit-файле)
|
||||
|
||||
### Embeddings не создаются
|
||||
|
||||
```bash
|
||||
# Проверь подключение к OpenAI через FNA
|
||||
sudo -u zbrain bash -c '
|
||||
source /etc/zbrain/.env
|
||||
curl --proxy "$HTTPS_PROXY" https://api.openai.com/v1/models -H "Authorization: Bearer $OPENAI_API_KEY"
|
||||
'
|
||||
```
|
||||
|
||||
### Postgres падает
|
||||
|
||||
```bash
|
||||
# Проверь логи
|
||||
sudo tail -100 /var/log/postgresql/postgresql-16-main.log
|
||||
|
||||
# Проверь disk space
|
||||
df -h /var/lib/postgresql/
|
||||
|
||||
# Проверь shared_buffers vs RAM
|
||||
sudo -u postgres psql -c 'SHOW shared_buffers; SHOW effective_cache_size;'
|
||||
```
|
||||
|
||||
### Брейн занимает слишком много места
|
||||
|
||||
```bash
|
||||
# Размер по таблицам
|
||||
sudo -u postgres psql -d gbrain_zetit -c "
|
||||
SELECT
|
||||
schemaname || '.' || tablename AS table,
|
||||
pg_size_pretty(pg_total_relation_size(schemaname || '.' || tablename)) AS size
|
||||
FROM pg_tables
|
||||
WHERE schemaname = 'public'
|
||||
ORDER BY pg_total_relation_size(schemaname || '.' || tablename) DESC;
|
||||
"
|
||||
```
|
||||
|
||||
Если эмбеддинги занимают слишком много — можно перейти на `text-embedding-3-small` (вместо `large`), это даст 1536 → 1024 dim и -33% места.
|
||||
@@ -0,0 +1,319 @@
|
||||
# Operations Guide
|
||||
|
||||
Документ для оператора ZBrain (admin) — повседневные задачи, типовые проблемы, runbook.
|
||||
|
||||
## Чек-лист после деплоя
|
||||
|
||||
После завершения [DEPLOYMENT.md](DEPLOYMENT.md):
|
||||
|
||||
- [ ] Все systemd unit'ы для брейнов активны: `systemctl status 'zbrain-gbrain-*'`
|
||||
- [ ] Docker контейнеры brainhub запущены: `docker compose -f /opt/zbrain/deploy/docker/docker-compose.yml ps`
|
||||
- [ ] Nginx обслуживает оба домена: `curl -H "Host: brain.zetit.local" http://localhost/health`
|
||||
- [ ] Cron для backup'ов настроен: `sudo crontab -l | grep backup`
|
||||
- [ ] Логи пишутся: `ls -lh /var/log/zbrain/`
|
||||
- [ ] Disk usage в норме: `df -h /var/lib/postgresql /var/lib/zbrain /var/log`
|
||||
|
||||
## Ежедневные задачи (автоматические)
|
||||
|
||||
### Backup (cron, 03:00)
|
||||
|
||||
```cron
|
||||
0 3 * * * root /opt/zbrain/scripts/backup.sh
|
||||
```
|
||||
|
||||
Что проверять раз в неделю:
|
||||
- `ls -lh /var/backups/zbrain/` — есть свежий бэкап
|
||||
- `cat /var/log/zbrain/backup.log | tail -20` — нет ошибок
|
||||
- Раз в месяц — тестовое восстановление на отдельной VM
|
||||
|
||||
### Sync (опционально, cron, 06:00 / 18:00)
|
||||
|
||||
Если источники не настроены через UI с расписанием, можно запускать вручную через scripts/sync-all.sh (будет добавлено в Sprint 4).
|
||||
|
||||
## Типовые задачи
|
||||
|
||||
### Создать нового пользователя
|
||||
|
||||
Через UI:
|
||||
1. Залогинься как owner
|
||||
2. /users → New user
|
||||
3. Email + role (admin / editor / viewer)
|
||||
4. Скопировать temp password из ответа
|
||||
|
||||
Через CLI (только owner):
|
||||
```bash
|
||||
sudo docker exec zbrain-api node -e '
|
||||
// TODO: команда будет добавлена в Sprint 7
|
||||
'
|
||||
```
|
||||
|
||||
### Создать новый брейн
|
||||
|
||||
```bash
|
||||
sudo bash /opt/zbrain/scripts/create-brain.sh <name> "<display name>" <port>
|
||||
|
||||
# Пример
|
||||
sudo bash /opt/zbrain/scripts/create-brain.sh fontvielle "Fontvielle Infrastructure" 3005
|
||||
```
|
||||
|
||||
### Импорт большого корпуса в брейн
|
||||
|
||||
```bash
|
||||
sudo -u zbrain bash -c '
|
||||
export PATH=$HOME/.bun/bin:$PATH
|
||||
source /etc/zbrain/.env
|
||||
export DATABASE_URL=$(cat /var/lib/zbrain/brains/zetit/config.json | jq -r .database_url)
|
||||
cd /var/lib/zbrain/gbrain
|
||||
bun run gbrain import /path/to/markdown/
|
||||
'
|
||||
```
|
||||
|
||||
Для очень больших корпусов (>10000 страниц) — лучше через UI после Sprint 4, там SSE прогресс.
|
||||
|
||||
### Обновить gbrain до новой версии
|
||||
|
||||
1. **На рабочей машине** (с GitHub доступом):
|
||||
```bash
|
||||
git clone --bare https://github.com/garrytan/gbrain.git
|
||||
cd gbrain.git
|
||||
git push --mirror git@git.zetit.ru:zuevav/gbrain-mirror.git
|
||||
```
|
||||
|
||||
2. **На VM ZBrain** — проверить changelog upstream'а на breaking changes, особенно в БД схеме.
|
||||
|
||||
3. Создать ADR в `docs/DECISIONS/` если есть значимые изменения.
|
||||
|
||||
4. Останавливаем все брейны:
|
||||
```bash
|
||||
sudo systemctl stop 'zbrain-gbrain-*'
|
||||
```
|
||||
|
||||
5. Обновляем gbrain:
|
||||
```bash
|
||||
sudo -u zbrain bash -c '
|
||||
cd /var/lib/zbrain/gbrain
|
||||
git fetch
|
||||
git checkout v0.27.0 # новая версия
|
||||
export PATH=$HOME/.bun/bin:$PATH
|
||||
export HTTPS_PROXY=$(grep HTTPS_PROXY /etc/zbrain/.env | cut -d= -f2-)
|
||||
bun install
|
||||
'
|
||||
```
|
||||
|
||||
6. Применяем миграции каждого брейна:
|
||||
```bash
|
||||
for brain in zetit telerapharma personal community; do
|
||||
sudo -u zbrain bash -c "
|
||||
export PATH=\$HOME/.bun/bin:\$PATH
|
||||
export DATABASE_URL=\$(cat /var/lib/zbrain/brains/$brain/config.json | jq -r .database_url)
|
||||
cd /var/lib/zbrain/gbrain
|
||||
bun run gbrain migrate
|
||||
"
|
||||
done
|
||||
```
|
||||
|
||||
7. Запускаем брейны:
|
||||
```bash
|
||||
sudo systemctl start 'zbrain-gbrain-*'
|
||||
```
|
||||
|
||||
8. Проверяем health.
|
||||
|
||||
### Восстановление из бэкапа
|
||||
|
||||
См. `scripts/restore.sh`. Краткая инструкция:
|
||||
|
||||
```bash
|
||||
sudo bash /opt/zbrain/scripts/restore.sh 20260520-031500
|
||||
# спросит путь к age приватному ключу
|
||||
```
|
||||
|
||||
**ВАЖНО:** после restore удалить age ключ с VM:
|
||||
```bash
|
||||
shred -u /tmp/backup.age
|
||||
```
|
||||
|
||||
### Реактивация заблокированного пользователя
|
||||
|
||||
Если пользователь забыл пароль или нужно сбросить:
|
||||
|
||||
```sql
|
||||
-- Через UI пока нет, делаем напрямую
|
||||
sudo -u postgres psql -d brainhub
|
||||
|
||||
-- Сгенерировать новый bcrypt hash (cost=12)
|
||||
-- Можно через: echo 'newpassword' | htpasswd -bnBC 12 "" -
|
||||
UPDATE users
|
||||
SET password_hash = '<new-bcrypt-hash>',
|
||||
must_change_password = true
|
||||
WHERE email = 'user@example.com';
|
||||
```
|
||||
|
||||
### Срочный отзыв токена
|
||||
|
||||
Если подозрение на утечку токена и нельзя ждать 30 секунд кэша:
|
||||
|
||||
```bash
|
||||
# Через UI: /tokens → Revoke. Это уже делает invalidate cache.
|
||||
|
||||
# Срочное через API:
|
||||
curl -X POST http://brain.zetit.local/api/admin/cache/invalidate-token \
|
||||
-H "Authorization: Bearer <admin-session-token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"tokenPrefix":"brain_pat_a8f3"}'
|
||||
|
||||
# Полный сброс кэша токенов (drastic):
|
||||
sudo systemctl restart docker
|
||||
sudo docker compose restart brainhub-api
|
||||
```
|
||||
|
||||
## Мониторинг
|
||||
|
||||
### Что смотреть в /var/log/zbrain/
|
||||
|
||||
```bash
|
||||
# Live tail всех логов
|
||||
sudo tail -F /var/log/zbrain/*.log
|
||||
|
||||
# Ошибки за последний час
|
||||
sudo journalctl --since "1 hour ago" -u 'zbrain-gbrain-*' | grep -i error
|
||||
|
||||
# Brainhub access pattern
|
||||
sudo tail -1000 /var/log/zbrain/brainhub.log | jq -r 'select(.req.url) | "\(.time) \(.req.method) \(.req.url) → \(.res.statusCode)"'
|
||||
```
|
||||
|
||||
### Метрики Postgres
|
||||
|
||||
```bash
|
||||
# Connection count
|
||||
sudo -u postgres psql -c 'SELECT count(*), state FROM pg_stat_activity GROUP BY state;'
|
||||
|
||||
# Размер БД
|
||||
sudo -u postgres psql -c "
|
||||
SELECT datname, pg_size_pretty(pg_database_size(datname))
|
||||
FROM pg_database WHERE datname IN ('brainhub','gbrain_zetit','gbrain_telerapharma','gbrain_personal','gbrain_community')
|
||||
ORDER BY pg_database_size(datname) DESC;
|
||||
"
|
||||
|
||||
# Топ медленных запросов
|
||||
sudo -u postgres psql -c '
|
||||
SELECT calls, mean_exec_time, query
|
||||
FROM pg_stat_statements
|
||||
ORDER BY mean_exec_time DESC LIMIT 10;
|
||||
' # если pg_stat_statements включён
|
||||
```
|
||||
|
||||
### Disk usage
|
||||
|
||||
```bash
|
||||
# По брейнам
|
||||
du -sh /var/lib/zbrain/brains/*/
|
||||
|
||||
# Postgres data
|
||||
sudo du -sh /var/lib/postgresql/16/main/
|
||||
|
||||
# Логи
|
||||
sudo du -sh /var/log/zbrain/ /var/log/postgresql/ /var/log/nginx/
|
||||
```
|
||||
|
||||
Если /var/log пухнет — настроить logrotate.
|
||||
|
||||
## Runbook'и инцидентов
|
||||
|
||||
### Сценарий: брейн не отвечает на MCP запросы
|
||||
|
||||
**Симптомы:** Claude Code получает 502 от /mcp/<brain>, в UI брейн помечен красным.
|
||||
|
||||
```bash
|
||||
# 1. Проверить systemd unit
|
||||
sudo systemctl status zbrain-gbrain-<brain>
|
||||
|
||||
# 2. Если down - последние логи
|
||||
sudo journalctl -u zbrain-gbrain-<brain> -n 100 --no-pager
|
||||
|
||||
# 3. Если crash loop - проверить Postgres connection
|
||||
sudo -u postgres psql -d gbrain_<brain> -c 'SELECT version()'
|
||||
|
||||
# 4. Если ОК - перезапустить
|
||||
sudo systemctl restart zbrain-gbrain-<brain>
|
||||
|
||||
# 5. Если не помогло - временно отключить и эскалировать
|
||||
sudo systemctl stop zbrain-gbrain-<brain>
|
||||
```
|
||||
|
||||
### Сценарий: Postgres недоступен
|
||||
|
||||
**Симптомы:** brainhub возвращает 503, все брейны лежат.
|
||||
|
||||
```bash
|
||||
# 1. Статус
|
||||
sudo systemctl status postgresql
|
||||
|
||||
# 2. Если crashed - проверить логи
|
||||
sudo tail -200 /var/log/postgresql/postgresql-16-main.log
|
||||
|
||||
# 3. Частые причины:
|
||||
# a) Disk full → освободить место (старые WAL, логи)
|
||||
df -h /var/lib/postgresql
|
||||
sudo du -sh /var/lib/postgresql/16/main/pg_wal/
|
||||
|
||||
# b) OOM → проверить dmesg
|
||||
sudo dmesg | tail -50 | grep -i 'killed process'
|
||||
|
||||
# c) Corrupt → restore из бэкапа
|
||||
```
|
||||
|
||||
### Сценарий: исчерпался лимит OpenAI/Anthropic API
|
||||
|
||||
**Симптомы:** sync падает с 429, новые pages не получают embeddings.
|
||||
|
||||
```bash
|
||||
# Проверка через FNA прокси
|
||||
sudo -u zbrain bash -c '
|
||||
source /etc/zbrain/.env
|
||||
curl --proxy "$HTTPS_PROXY" \
|
||||
https://api.openai.com/v1/dashboard/billing/usage \
|
||||
-H "Authorization: Bearer $OPENAI_API_KEY"
|
||||
'
|
||||
|
||||
# Действия:
|
||||
# 1. Пополнить баланс на платформе OpenAI/Anthropic
|
||||
# 2. Или временно поставить на паузу автоматический sync
|
||||
# 3. Прикинуть стоимость re-embed: ~$0.65 на 7500 страниц для text-embedding-3-large
|
||||
```
|
||||
|
||||
### Сценарий: подозрение на утечку токена
|
||||
|
||||
```bash
|
||||
# 1. Найти токен в UI: /tokens → найти по prefix
|
||||
# 2. Посмотреть активность: /audit?actor_type=token&actor_id=<id>
|
||||
# 3. Revoke + invalidate cache
|
||||
# 4. Создать новый токен с теми же scope, передать пользователю
|
||||
# 5. Анализ через audit log:
|
||||
sudo -u postgres psql -d brainhub -c "
|
||||
SELECT created_at, ip, action, payload
|
||||
FROM audit_log
|
||||
WHERE actor_type='token' AND actor_id='<token-id>'
|
||||
ORDER BY created_at DESC LIMIT 100;
|
||||
"
|
||||
# 6. Если был доступ из неожиданного IP - эскалация (анализ что было прочитано/изменено)
|
||||
```
|
||||
|
||||
## Регулярные процедуры
|
||||
|
||||
### Раз в неделю
|
||||
- Проверить freshness бэкапов и логи backup.log
|
||||
- Проверить disk usage
|
||||
- Глянуть audit log на необычную активность
|
||||
|
||||
### Раз в месяц
|
||||
- Тестовый restore на отдельной VM
|
||||
- Review обновлений gbrain upstream
|
||||
- Обновить ОС (`apt update && apt upgrade`)
|
||||
- Rotate session secrets (опционально, но повышает security posture)
|
||||
|
||||
### Раз в квартал
|
||||
- Penetration test публичного контура (хотя бы на nikto/zap-baseline)
|
||||
- Review активных токенов, удаление неиспользуемых
|
||||
- Review активных пользователей, удаление неактивных
|
||||
- Capacity planning: смотреть рост БД и проектировать апгрейд VM при необходимости
|
||||
+274
@@ -0,0 +1,274 @@
|
||||
# ZBrain Roadmap
|
||||
|
||||
Документ для Claude Code: пошаговый план реализации с критериями готовности каждого спринта. Подходит как для последовательной разработки, так и для параллельной (некоторые спринты независимы).
|
||||
|
||||
## Принципы
|
||||
|
||||
- Каждый спринт заканчивается **рабочей фичей**, которую можно показать
|
||||
- Каждый спринт мерж в `develop`, в `main` идут тегированные релизы
|
||||
- Любой Claude Code инстанс может прочитать этот файл и понять, что делать дальше
|
||||
- ADR (Architecture Decision Records) пишутся в `docs/DECISIONS/` для нестандартных решений
|
||||
|
||||
---
|
||||
|
||||
## Sprint 0: Инфраструктура VM (1 вечер)
|
||||
|
||||
**Цель**: Готовая VM, на которой работает Postgres и установлен gbrain.
|
||||
|
||||
**Задачи:**
|
||||
- [ ] Создать VM: 4 vCPU / 8 GB RAM / 80 GB SSD / Ubuntu 22.04 LTS
|
||||
- [ ] Назначить статический IP в сети ZETIT
|
||||
- [ ] Добавить DNS: `brain.zetit.local` (внутренний) и `brain.zetit.ru` (внешний)
|
||||
- [ ] Запустить `scripts/bootstrap-vm.sh`
|
||||
- [ ] Создать Postgres пользователя `brainhub` с паролем
|
||||
- [ ] Сгенерировать секреты для `.env` (SESSION_SECRET, JWT_SECRET, TOKEN_ENCRYPTION_KEY)
|
||||
- [ ] Получить API ключи: OPENAI_API_KEY, ANTHROPIC_API_KEY
|
||||
|
||||
**Критерии готовности:**
|
||||
- `systemctl status postgresql` показывает active
|
||||
- `sudo -u postgres psql -d brainhub -c 'SELECT * FROM pg_extension'` возвращает vector, pg_trgm, pgcrypto
|
||||
- `sudo -u zbrain bash -c 'PATH=$HOME/.bun/bin:$PATH bun --version'` работает
|
||||
- `/etc/zbrain/.env` существует, заполнен, права 600
|
||||
|
||||
**Документировать:** реальные параметры VM (IP, hostname, дата создания) в `docs/INFRASTRUCTURE.md`
|
||||
|
||||
---
|
||||
|
||||
## Sprint 1: Первый gbrain instance (1 день)
|
||||
|
||||
**Цель**: Один работающий gbrain instance с импортированными тестовыми данными.
|
||||
|
||||
**Задачи:**
|
||||
- [ ] Создать gbrain mirror в `git.zetit.ru/zuevav/gbrain-mirror`
|
||||
- [ ] Запустить `scripts/create-brain.sh zetit "ZETIT MSP" 3001`
|
||||
- [ ] Импортировать тестовый корпус (например, какие-то существующие markdown файлы)
|
||||
- [ ] Проверить `curl http://localhost:3001/health` или эквивалент
|
||||
- [ ] Прицепить локальный Claude Code напрямую к этому gbrain (без brainhub-прокси)
|
||||
- [ ] Выполнить пару query и убедиться что работает
|
||||
|
||||
**Критерии готовности:**
|
||||
- `systemctl status zbrain-gbrain-zetit` показывает active
|
||||
- В БД есть страницы и эмбеддинги: `SELECT count(*) FROM pages, count(*) FROM content_chunks WHERE embedding IS NOT NULL`
|
||||
- Claude Code успешно делает MCP запросы
|
||||
|
||||
**Известные риски:**
|
||||
- gbrain v0.26 переехал на новый OAuth - возможно поломались CLI флаги. Зафиксировать рабочую версию тегом.
|
||||
- FNA-прокси может блокировать какие-то OpenAI/Anthropic эндпоинты - проверить `curl --proxy ...` до запуска import.
|
||||
|
||||
---
|
||||
|
||||
## Sprint 2: Brainhub каркас + локальный auth (3-4 дня)
|
||||
|
||||
**Цель**: Работающий веб-интерфейс с login/logout, без функционала брейнов.
|
||||
|
||||
**Задачи:**
|
||||
|
||||
### apps/api (backend)
|
||||
- [ ] Express + TypeScript скелет
|
||||
- [ ] Структура: `routes/`, `services/`, `middleware/`, `db/`, `lib/`
|
||||
- [ ] ORM выбрать: Drizzle (рекомендую за TS-first) или Prisma
|
||||
- [ ] Миграции: создать таблицы `users`, `sessions`
|
||||
- [ ] POST /api/auth/register (только для owner-команды, остальные через UI)
|
||||
- [ ] POST /api/auth/login (email + password, bcrypt)
|
||||
- [ ] POST /api/auth/logout
|
||||
- [ ] GET /api/auth/me (current user)
|
||||
- [ ] Session middleware (cookie + refresh token rotation)
|
||||
- [ ] RBAC middleware: `requireRole('admin')`, `requireRole('owner')`
|
||||
|
||||
### apps/web (frontend)
|
||||
- [ ] React + Vite + TypeScript + Tailwind + shadcn/ui
|
||||
- [ ] Layout: sidebar + header + content
|
||||
- [ ] Страницы: /login, /dashboard (заглушка), /profile
|
||||
- [ ] React Router v6
|
||||
- [ ] React Query для API
|
||||
- [ ] Auth context + protected routes
|
||||
|
||||
**Критерии готовности:**
|
||||
- На свежей БД создаётся initial owner из ENV (INITIAL_OWNER_EMAIL/PASSWORD)
|
||||
- Можно войти через UI на /login
|
||||
- /dashboard защищён, без auth редиректит на /login
|
||||
- Cookie с session token, при logout удаляется
|
||||
- Refresh token rotation работает (логин не истекает через 15 минут активной сессии)
|
||||
|
||||
---
|
||||
|
||||
## Sprint 3: MCP Proxy + Tokens (2-3 дня)
|
||||
|
||||
**Цель**: Claude Code на любом сервере подключается через brainhub, а не напрямую к gbrain.
|
||||
|
||||
**Задачи:**
|
||||
|
||||
### packages/mcp-proxy
|
||||
- [ ] Express router, маунтится на `/mcp/:brain`
|
||||
- [ ] Извлечение Bearer token из заголовков
|
||||
- [ ] Кэш токенов в памяти (30s TTL) - не ходить в БД на каждый запрос
|
||||
- [ ] Проверка: revoked, expired, IP allowlist, scope
|
||||
- [ ] Резолв `:brain` → `localhost:PORT` через таблицу `brains`
|
||||
- [ ] Прокси через `http-proxy-middleware` или nativный stream
|
||||
- [ ] In-memory очередь для audit log + batch insert (раз в секунду)
|
||||
- [ ] Throttled update `last_used_at` (не чаще раза в минуту на токен)
|
||||
|
||||
### apps/api: токены
|
||||
- [ ] Таблица `access_tokens` (см. docs/SECURITY.md)
|
||||
- [ ] POST /api/tokens (создание - возвращает plaintext token ОДИН РАЗ)
|
||||
- [ ] GET /api/tokens (список с prefix, без full token)
|
||||
- [ ] DELETE /api/tokens/:id (revoke - soft delete с revoked_at)
|
||||
- [ ] GET /api/tokens/:id/usage (статистика)
|
||||
|
||||
### apps/web: UI для токенов
|
||||
- [ ] /tokens страница со списком
|
||||
- [ ] Создание токена: имя + scope checkboxes + TTL + IP allowlist
|
||||
- [ ] После создания - модалка с токеном и кнопкой copy (показывается один раз!)
|
||||
- [ ] Revoke с подтверждением
|
||||
|
||||
**Критерии готовности:**
|
||||
- Создал токен в UI с scope `mcp:read:zetit`
|
||||
- Подключил Claude Code: `mcpServers.zbrain.url = http://brain.zetit.local/mcp/zetit`, header `Authorization: Bearer brain_pat_...`
|
||||
- Запрос идёт через brainhub, в audit log записывается
|
||||
- Revoke токена через UI → следующий запрос Claude Code получает 401
|
||||
- При двух токенах одного scope работают оба
|
||||
|
||||
---
|
||||
|
||||
## Sprint 4: CRUD брейнов + источники + sync (2-3 дня)
|
||||
|
||||
**Цель**: Создание новых брейнов через UI, добавление и синхронизация источников.
|
||||
|
||||
**Задачи:**
|
||||
|
||||
### apps/api
|
||||
- [ ] Таблицы `brains`, `sources`
|
||||
- [ ] POST /api/brains - создание (внутренний вызов `scripts/create-brain.sh` или эквивалент на TS)
|
||||
- [ ] GET /api/brains - список со статистикой (page_count, embed_coverage)
|
||||
- [ ] GET /api/brains/:id - детали
|
||||
- [ ] DELETE /api/brains/:id - удаление (с подтверждением, дамп перед удалением)
|
||||
- [ ] POST /api/brains/:id/sources - добавление источника (git, local_dir)
|
||||
- [ ] POST /api/brains/:id/sources/:sourceId/sync - запуск sync
|
||||
- [ ] GET /api/brains/:id/sources/:sourceId/sync/stream - SSE с прогрессом
|
||||
- [ ] Worker queue для sync (bullmq или нативный setInterval)
|
||||
- [ ] Планировщик cron-based sync (node-cron)
|
||||
|
||||
### apps/web
|
||||
- [ ] /brains страница со списком
|
||||
- [ ] /brains/new форма создания
|
||||
- [ ] /brains/:id - tabs: Overview, Sources, Browse, Query, Activity
|
||||
- [ ] Sources tab: список с status, кнопки Sync now / Edit / Delete
|
||||
- [ ] Sync с realtime прогрессом через SSE
|
||||
- [ ] Подсветка статусов: green/yellow/red
|
||||
|
||||
**Критерии готовности:**
|
||||
- Создать брейн `community` через UI → автоматически создаётся БД, запускается gbrain, регистрируется systemd unit
|
||||
- Добавить git source → запустить sync → видеть прогресс в реальном времени
|
||||
- Расписание sync каждые 6 часов работает
|
||||
- Удаление брейна делает дамп и убирает все артефакты
|
||||
|
||||
**Важно:** удаление брейна - irreversible операция, обязательно подтверждение через ввод имени брейна (как в GitHub).
|
||||
|
||||
---
|
||||
|
||||
## Sprint 5: Dashboard + Projects + Connect (2-3 дня)
|
||||
|
||||
**Цель**: Полноценный UI с визуализацией проектов и copy-paste готовыми сниппетами.
|
||||
|
||||
**Задачи:**
|
||||
|
||||
### apps/api
|
||||
- [ ] Таблицы `projects`, `project_connections`
|
||||
- [ ] CRUD endpoints для projects
|
||||
- [ ] POST /api/projects/:id/connections - привязка проекта к брейну с указанием токена
|
||||
- [ ] GET /api/connect/snippets/:brainId?client=claude-code|cursor|cli - генерация сниппетов
|
||||
|
||||
### apps/web
|
||||
- [ ] /dashboard - сводка: количество брейнов, total pages, active sync jobs, recent activity
|
||||
- [ ] /dashboard карточки брейнов с цифрами
|
||||
- [ ] /projects - таблица проектов с привязкой к брейнам
|
||||
- [ ] /projects/:id - детали + кнопка "Connect new brain"
|
||||
- [ ] /brains/:id/connect - страница с готовыми сниппетами:
|
||||
- Claude Code JSON (для `~/.claude/mcp.json` или `.claude/mcp.json` в проекте)
|
||||
- Cursor конфиг
|
||||
- Bash one-liner для CLI
|
||||
- QR код с готовой строкой подключения
|
||||
- [ ] Copy-to-clipboard на каждом блоке
|
||||
- [ ] Глобальный поиск по всем брейнам (с уважением scope)
|
||||
|
||||
**Критерии готовности:**
|
||||
- Dashboard загружается за <500ms
|
||||
- В Projects вижу таблицу: ZetitConnect → [zetit (write), personal (read)]
|
||||
- На странице Connect - готовая JSON структура с реальным URL и токеном, копируется одним кликом
|
||||
- QR-код корректно сканируется и содержит JSON
|
||||
|
||||
---
|
||||
|
||||
## Sprint 6: OAuth + публичный контур (2-3 дня)
|
||||
|
||||
**Цель**: Авторизация через Yandex/GitHub OAuth, публичный endpoint для MCP.
|
||||
|
||||
**Задачи:**
|
||||
|
||||
### apps/api
|
||||
- [ ] Зарегистрировать OAuth приложения у Yandex и GitHub
|
||||
- [ ] passport.js + passport-yandex + passport-github2
|
||||
- [ ] OAuth flow: /oauth/yandex, /oauth/yandex/callback (аналогично GitHub)
|
||||
- [ ] Привязка к existing user по email match (с подтверждением)
|
||||
- [ ] Поддержка регистрации нового пользователя через OAuth (опционально, по INVITE_ONLY флагу)
|
||||
- [ ] Отдельный Express app на 3010 порту, монтирует только /mcp и /oauth
|
||||
|
||||
### deploy
|
||||
- [ ] Получить TLS сертификат от Let's Encrypt для brain.zetit.ru
|
||||
- [ ] Включить nginx public server-блок
|
||||
- [ ] Проверить, что внутренний и публичный API не пересекаются (нет утечки UI на публичный)
|
||||
|
||||
**Критерии готовности:**
|
||||
- Залогиниться через "Sign in with Yandex" - работает
|
||||
- На публичном endpoint `https://brain.zetit.ru/api/brains` → 404
|
||||
- На публичном endpoint `https://brain.zetit.ru/mcp/zetit` → работает с токеном
|
||||
- TLS A+ rating на SSL Labs
|
||||
|
||||
---
|
||||
|
||||
## Sprint 7: Hardening (2-3 дня)
|
||||
|
||||
**Цель**: Production-ready: бэкапы, мониторинг, 2FA, документация.
|
||||
|
||||
**Задачи:**
|
||||
- [ ] /audit страница в UI с фильтрами (actor, action, resource, date range)
|
||||
- [ ] CSV export audit log
|
||||
- [ ] 2FA (TOTP) опционально для каждого пользователя
|
||||
- [ ] Проверить и настроить `scripts/backup.sh`, добавить в cron
|
||||
- [ ] Тестовое восстановление из бэкапа на отдельной VM
|
||||
- [ ] Prometheus endpoint /metrics: request rate, error rate, db connections, queue depth
|
||||
- [ ] Health endpoint с глубокой проверкой (DB connectivity, gbrain instances)
|
||||
- [ ] Alerts на падение sync, на error rate > N
|
||||
- [ ] Documentation: docs/API.md, docs/SECURITY.md, docs/OPERATIONS.md
|
||||
- [ ] Load test: 100 RPS на /mcp, что выдерживает
|
||||
|
||||
**Критерии готовности:**
|
||||
- Восстановление из бэкапа прошло успешно
|
||||
- Включил 2FA на owner аккаунте, работает
|
||||
- /metrics возвращает prometheus formatted text
|
||||
- Audit log имеет все важные события: brain.create, brain.delete, token.create, token.revoke, user.login, user.logout, mcp.call
|
||||
|
||||
---
|
||||
|
||||
## После Sprint 7: миграция данных
|
||||
|
||||
**Цель**: Перенести всё что нужно в соответствующие брейны.
|
||||
|
||||
- [ ] Создать брейны: zetit, telerapharma, personal, community (если ещё не созданы)
|
||||
- [ ] Источники для zetit: ZetitConnect docs repo, ZETIT internal wiki
|
||||
- [ ] Источники для telerapharma: runbook'и Exchange/MikroTik/1C
|
||||
- [ ] Источники для personal: личный markdown garden, "Хлеб и молоко" docs
|
||||
- [ ] Источники для community: документы Смоленская 10
|
||||
- [ ] Переключить Claude Code на всех серверах с локального чтения на gbrain через brainhub
|
||||
- [ ] Удалить дублированную документацию из локальных копий (оставить только в источниках)
|
||||
|
||||
---
|
||||
|
||||
## Дальнейшее развитие
|
||||
|
||||
- Импорт из Notion, Obsidian, Confluence через gbrain skills
|
||||
- Webhook'и: уведомления в Slack/Telegram при критичных событиях
|
||||
- API для интеграций с другими системами (1С, FortiGate logs)
|
||||
- Mobile-friendly UI (адаптив или отдельная PWA)
|
||||
- Multi-user collaboration на одном брейне (комментарии, suggestions)
|
||||
- Версионирование промптов и skills через gbrain
|
||||
@@ -0,0 +1,357 @@
|
||||
# Безопасность ZBrain
|
||||
|
||||
## Модель угроз
|
||||
|
||||
**Что защищаем:**
|
||||
- Содержимое брейнов (особенно TeleraPharma — есть NDA и чувствительная инфраструктурная информация)
|
||||
- Учётные записи пользователей
|
||||
- API ключи (OpenAI, Anthropic) — финансовые потери при утечке
|
||||
- MCP токены — могут давать доступ к чтению/записи брейнов
|
||||
|
||||
**От кого защищаем:**
|
||||
- **Внешний атакующий** через публичный endpoint brain.zetit.ru
|
||||
- **Скомпрометированный сервер клиента** с украденным MCP токеном
|
||||
- **Случайная утечка** через копирование токена в git, log файлы, скриншот
|
||||
- **Внутренний пользователь** с правами viewer, который хочет получить admin
|
||||
|
||||
**От чего не защищаемся (нет ресурсов):**
|
||||
- Физический доступ к VM (предполагаем что гипервизор/датацентр доверенный)
|
||||
- Скомпрометированный root на самой VM
|
||||
- 0-day в Postgres, Node.js, библиотеках
|
||||
|
||||
## Роли пользователей
|
||||
|
||||
| Роль | Управление пользователями | Управление брейнами | Управление источниками | Управление токенами | Просмотр audit | UI |
|
||||
|------|---------------------------|---------------------|------------------------|--------------------|-----|---|
|
||||
| **owner** | ✓ Полное | ✓ Полное | ✓ Полное | ✓ Полное | ✓ Все | ✓ |
|
||||
| **admin** | Только viewer/editor | ✓ Полное | ✓ Полное | ✓ Свои + смотреть чужие | ✓ Все | ✓ |
|
||||
| **editor** | ✗ | ✗ Создавать нельзя; редактировать содержимое — да | ✓ В назначенных брейнах | ✓ Свои с scope :read и :write | ✓ Свои действия | ✓ |
|
||||
| **viewer** | ✗ | ✗ | ✗ | ✓ Свои только :read | ✓ Свои действия | ✓ Read-only |
|
||||
| **agent** (не человек) | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ MCP only |
|
||||
|
||||
В первой версии у нас один owner — это ты. Остальные роли — для будущего.
|
||||
|
||||
## MCP Токены
|
||||
|
||||
### Формат
|
||||
|
||||
```
|
||||
brain_pat_<random40chars>
|
||||
```
|
||||
|
||||
Префикс `brain_pat_` (Personal Access Token) — чтобы легко grep'ать в логах и автоматических сканерах секретов.
|
||||
|
||||
### Хранение
|
||||
|
||||
- Plain text токен **никогда** не хранится
|
||||
- Хранится `token_hash = SHA-256(token)` и `token_prefix = первые 8 символов после brain_pat_` для UI
|
||||
- При создании токена UI показывает full token **один раз**, потом только prefix
|
||||
|
||||
### Scope формат
|
||||
|
||||
```
|
||||
mcp:<level>:<brain-name>
|
||||
```
|
||||
|
||||
Уровни:
|
||||
- `mcp:read:<brain>` — query, search, get_page, get_tags, get_links, get_backlinks, get_timeline, get_stats, get_health, get_versions
|
||||
- `mcp:write:<brain>` — всё из read + put_page, add_tag, remove_tag, add_link, remove_link, add_timeline_entry, sync_brain
|
||||
- `mcp:admin:<brain>` — всё из write + delete_page, revert_version
|
||||
|
||||
Wildcards:
|
||||
- `mcp:read:*` — read доступ ко всем брейнам (только owner)
|
||||
- `mcp:*:zetit` — все права на zetit брейн
|
||||
|
||||
Один токен может иметь несколько scope, например:
|
||||
```json
|
||||
{
|
||||
"scopes": ["mcp:read:zetit", "mcp:write:personal"]
|
||||
}
|
||||
```
|
||||
|
||||
### IP Allowlist
|
||||
|
||||
Опционально для каждого токена — список IP/CIDR откуда разрешён доступ.
|
||||
|
||||
Для production токенов (на серверах клиентов) — **обязательно** указывать IP клиентского сервера.
|
||||
|
||||
### TTL
|
||||
|
||||
- По умолчанию: 90 дней
|
||||
- Опции в UI: 7 дней, 30 дней, 90 дней, 1 год, без срока
|
||||
- "Без срока" доступен только owner и admin
|
||||
|
||||
### Revocation
|
||||
|
||||
Soft delete: `revoked_at = now()`.
|
||||
|
||||
Кэш в brainhub имеет TTL 30 секунд, поэтому отзыв вступает в силу в течение 30 секунд.
|
||||
|
||||
Для немедленного отзыва — `POST /api/admin/cache/invalidate-token`.
|
||||
|
||||
### Алгоритм валидации
|
||||
|
||||
```typescript
|
||||
async function validateToken(token: string, brain: string, requiredScope: 'read'|'write'|'admin'): Promise<TokenInfo> {
|
||||
// 1. Проверка формата
|
||||
if (!token.startsWith('brain_pat_')) throw new Error('Invalid token format');
|
||||
|
||||
const tokenHash = sha256(token);
|
||||
|
||||
// 2. Проверка в кэше
|
||||
const cached = tokenCache.get(tokenHash);
|
||||
if (cached) {
|
||||
if (cached.error) throw new Error(cached.error);
|
||||
return validateScope(cached, brain, requiredScope);
|
||||
}
|
||||
|
||||
// 3. DB lookup
|
||||
const dbToken = await db.tokens.findByHash(tokenHash);
|
||||
if (!dbToken) {
|
||||
tokenCache.set(tokenHash, {error: 'Not found'}, 30_000);
|
||||
throw new Error('Token not found');
|
||||
}
|
||||
|
||||
// 4. Проверки статуса
|
||||
if (dbToken.revoked_at) throw new Error('Token revoked');
|
||||
if (dbToken.expires_at && dbToken.expires_at < new Date()) throw new Error('Token expired');
|
||||
|
||||
// 5. IP allowlist
|
||||
if (dbToken.ip_allowlist?.length > 0) {
|
||||
const clientIp = getClientIp();
|
||||
if (!isIpInAllowlist(clientIp, dbToken.ip_allowlist)) {
|
||||
throw new Error('IP not allowed');
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Scope check
|
||||
validateScope(dbToken, brain, requiredScope);
|
||||
|
||||
// 7. Кэшируем positive result
|
||||
tokenCache.set(tokenHash, dbToken, 30_000);
|
||||
|
||||
// 8. Throttled last_used update
|
||||
scheduleLastUsedUpdate(dbToken.id);
|
||||
|
||||
return dbToken;
|
||||
}
|
||||
```
|
||||
|
||||
## Сессии
|
||||
|
||||
### Cookie
|
||||
|
||||
```
|
||||
Name: zbrain_session
|
||||
Value: <jwt-signed session id>
|
||||
HttpOnly: true
|
||||
Secure: true (только в production)
|
||||
SameSite: Lax
|
||||
Path: /
|
||||
Domain: brain.zetit.local или brain.zetit.ru
|
||||
```
|
||||
|
||||
JWT внутри cookie - только session id, не данные пользователя. Все данные подтягиваются из БД по session id.
|
||||
|
||||
### Refresh tokens
|
||||
|
||||
- Access JWT: 15 минут
|
||||
- Refresh token: 30 дней, ротируется при каждом использовании
|
||||
- Refresh token хранится в БД как hash (см. таблицу `sessions`)
|
||||
|
||||
### Logout
|
||||
|
||||
- Удаление cookie
|
||||
- `sessions.revoked_at = now()` для всех сессий пользователя (если "logout everywhere")
|
||||
|
||||
## OAuth
|
||||
|
||||
### Поддерживаемые провайдеры
|
||||
|
||||
1. **Yandex** (основной)
|
||||
2. **GitHub** (резервный)
|
||||
3. **Local email/password** (всегда доступен для owner)
|
||||
|
||||
### Привязка к существующему пользователю
|
||||
|
||||
Если OAuth возвращает email, который уже есть в users:
|
||||
- Если у пользователя нет привязки OAuth → требуется подтверждение пароля
|
||||
- Если есть привязка к другому провайдеру → ошибка ("Already linked to GitHub")
|
||||
- Если есть привязка к этому же провайдеру → login успешный
|
||||
|
||||
### Новые пользователи через OAuth
|
||||
|
||||
По умолчанию **отключено** (флаг `OAUTH_AUTO_REGISTER=false`).
|
||||
|
||||
В первой версии — только invite-based: owner создаёт пользователя с email, тот логинится через OAuth, происходит match по email.
|
||||
|
||||
## 2FA (TOTP)
|
||||
|
||||
Опционально, в Sprint 7.
|
||||
|
||||
- Секрет генерируется при включении, показывается QR-код для Google Authenticator / Authy
|
||||
- Сохраняется в `users.totp_secret` зашифрованным
|
||||
- 8 backup codes генерируются и показываются один раз
|
||||
- При логине после успешного password check → требуется TOTP код
|
||||
- Для OAuth-логина 2FA не запрашивается (полагаемся на 2FA провайдера)
|
||||
|
||||
## Audit Log
|
||||
|
||||
### Что пишем
|
||||
|
||||
Категории событий:
|
||||
|
||||
**Auth:**
|
||||
- `user.login.success` / `user.login.failure`
|
||||
- `user.logout`
|
||||
- `user.oauth.link` / `user.oauth.unlink`
|
||||
- `user.totp.enable` / `user.totp.disable`
|
||||
|
||||
**User management:**
|
||||
- `user.create`
|
||||
- `user.update`
|
||||
- `user.delete`
|
||||
- `user.role.change`
|
||||
|
||||
**Brain management:**
|
||||
- `brain.create`
|
||||
- `brain.update`
|
||||
- `brain.delete`
|
||||
- `brain.sync.start` / `brain.sync.complete` / `brain.sync.fail`
|
||||
|
||||
**Source management:**
|
||||
- `source.add`
|
||||
- `source.update`
|
||||
- `source.delete`
|
||||
|
||||
**Token management:**
|
||||
- `token.create` (с указанием scopes)
|
||||
- `token.revoke`
|
||||
|
||||
**MCP usage:**
|
||||
- `mcp.call` (rate-sampled: 1 запись на 10 вызовов на токен в минуту)
|
||||
|
||||
**Admin actions:**
|
||||
- `admin.cache.invalidate`
|
||||
- `admin.config.update`
|
||||
|
||||
### Формат
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: bigint, // serial
|
||||
actor_type: 'user' | 'token',
|
||||
actor_id: uuid, // user.id или token.id
|
||||
action: string, // 'brain.create', 'mcp.call', etc.
|
||||
resource_type: string?, // 'brain', 'token', 'source', etc.
|
||||
resource_id: string?, // ID объекта
|
||||
payload: jsonb, // дополнительные данные
|
||||
ip: inet?,
|
||||
user_agent: text?,
|
||||
created_at: timestamptz
|
||||
}
|
||||
```
|
||||
|
||||
### Хранение
|
||||
|
||||
- Хранятся **навсегда** в первой версии (для compliance)
|
||||
- В будущем — архив старее года в отдельную холодную БД
|
||||
- Индексы: `(actor_type, actor_id, created_at DESC)`, `(resource_type, resource_id, created_at DESC)`
|
||||
|
||||
### Batch insert
|
||||
|
||||
MCP-запросов будет много (тысячи в день). Не делаем INSERT на каждый запрос:
|
||||
|
||||
```typescript
|
||||
const auditQueue: AuditEntry[] = [];
|
||||
|
||||
// API endpoint:
|
||||
function logAudit(entry: AuditEntry) {
|
||||
auditQueue.push(entry);
|
||||
// не ждём insert
|
||||
}
|
||||
|
||||
// Worker раз в секунду:
|
||||
setInterval(async () => {
|
||||
if (auditQueue.length === 0) return;
|
||||
const batch = auditQueue.splice(0, 1000);
|
||||
await db.audit_log.insertMany(batch);
|
||||
}, 1000);
|
||||
|
||||
// graceful shutdown:
|
||||
process.on('SIGTERM', async () => {
|
||||
if (auditQueue.length > 0) {
|
||||
await db.audit_log.insertMany(auditQueue);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Сетевая безопасность
|
||||
|
||||
### Внутренний контур (brain.zetit.local)
|
||||
|
||||
- Доступ только из сети ZETIT или через VPN
|
||||
- Plain HTTP допустим (доверенная сеть)
|
||||
- Postgres слушает только localhost (127.0.0.1)
|
||||
- UFW: открыты только 22 (SSH), 80, 443
|
||||
|
||||
### Публичный контур (brain.zetit.ru)
|
||||
|
||||
- Только HTTPS, TLS 1.2+ с safe ciphers
|
||||
- Rate limiting на nginx уровне:
|
||||
- 30 r/s per IP (защита от подбора токенов)
|
||||
- 100 r/s per token (защита от runaway script)
|
||||
- Доступны только `/mcp/*`, `/oauth/*`, `/health`
|
||||
- Любой другой path → 404 без подсказок
|
||||
|
||||
### SSH
|
||||
|
||||
- Только ключи, password auth отключён
|
||||
- `PermitRootLogin no`
|
||||
- Fail2ban на /var/log/auth.log
|
||||
|
||||
## Защита секретов
|
||||
|
||||
### В коде
|
||||
|
||||
- НИКОГДА не коммитим `.env`, `*.key`, `*.pem`
|
||||
- `.gitignore` строгий
|
||||
- Pre-commit hook с git-secrets или talisman (опционально)
|
||||
|
||||
### На VM
|
||||
|
||||
- `/etc/zbrain/.env` — права 600, owner zbrain
|
||||
- Postgres пароли каждого брейна — в `/var/lib/zbrain/brains/<name>/config.json` (600, owner zbrain)
|
||||
- API ключи OpenAI/Anthropic — в .env, не в коде
|
||||
- TLS приватные ключи в `/etc/letsencrypt/live/` — стандартные права Let's Encrypt
|
||||
|
||||
### В БД
|
||||
|
||||
- Пароли пользователей — bcrypt с cost=12
|
||||
- MCP токены — SHA-256 hash (HMAC не нужен, потому что коллизии не критичны)
|
||||
- TOTP secrets — AES-128-GCM с ключом из TOKEN_ENCRYPTION_KEY
|
||||
- OAuth subjects — открытым текстом (это не секрет)
|
||||
|
||||
### Backup
|
||||
|
||||
- Полный бэкап шифруется age (или GPG) перед сохранением
|
||||
- Публичный ключ для шифрования — на VM в /etc/zbrain/backup.age.pub
|
||||
- Приватный ключ для расшифровки — **НЕ на VM**, хранится отдельно (KeePass / safe / другая машина)
|
||||
|
||||
## Чек-лист безопасности перед production
|
||||
|
||||
- [ ] Все пароли Postgres сильные (≥24 символа, generated)
|
||||
- [ ] `/etc/zbrain/.env` с правами 600
|
||||
- [ ] UFW активен, открыты только нужные порты
|
||||
- [ ] Fail2ban настроен для SSH и nginx
|
||||
- [ ] TLS сертификат валиден (A+ rating на ssllabs.com)
|
||||
- [ ] Postgres listen_addresses = 'localhost'
|
||||
- [ ] Создан age ключ для бэкапов, приватная часть НЕ на этой VM
|
||||
- [ ] Скрипт backup.sh в cron, протестирован restore
|
||||
- [ ] OAuth приложения зарегистрированы, callback URL'ы правильные
|
||||
- [ ] Owner аккаунт создан, password сильный, 2FA включена (после Sprint 7)
|
||||
- [ ] INITIAL_OWNER_PASSWORD сменён после первого логина
|
||||
- [ ] Audit log пишется и читается через UI
|
||||
- [ ] Test: попытка доступа к /api/* через brain.zetit.ru → 404
|
||||
- [ ] Test: запрос с revoked токеном → 401 в течение 30 секунд
|
||||
- [ ] Test: запрос с неверным scope → 403
|
||||
Reference in New Issue
Block a user