This commit is contained in:
zuevav
2026-05-20 19:33:02 +03:00
commit f4bca8449e
30 changed files with 4152 additions and 0 deletions
+211
View File
@@ -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 месяцев на реальных метриках.
+60
View File
@@ -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)** — лишняя сущность для проекта такого размера
+103
View File
@@ -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 на каждый брейн** — экстремальная изоляция, не нужна на нашем масштабе
+362
View File
@@ -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% места.
+319
View File
@@ -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
View File
@@ -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
+357
View File
@@ -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