main
This commit is contained in:
@@ -0,0 +1,178 @@
|
|||||||
|
# ZBrain — Claude Code Context
|
||||||
|
|
||||||
|
Этот файл читается Claude Code при работе в репозитории ZBrain. Описывает контекст проекта, конвенции, и где что искать.
|
||||||
|
|
||||||
|
## Что это за проект
|
||||||
|
|
||||||
|
ZBrain — централизованная база знаний для AI-агентов ZETIT. Это:
|
||||||
|
- **Веб-админка** для управления несколькими gbrain instance'ами
|
||||||
|
- **MCP-прокси** между Claude Code и gbrain с auth, rate limit, audit
|
||||||
|
- **Многотенантная архитектура** с изоляцией БД на каждый брейн
|
||||||
|
|
||||||
|
Полное описание — в [README.md](README.md), архитектура — в [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
|
||||||
|
|
||||||
|
## Контекст пользователя
|
||||||
|
|
||||||
|
Этот проект разрабатывается **Зуевым Алексеем (ZETIT, ИП)**. Окружение:
|
||||||
|
- **Продакшен серверы** работают на Ubuntu 22.04
|
||||||
|
- **Сеть** — за FNA-прокси (`fna.zetit.ru:3128`, логин `client_001`)
|
||||||
|
- **Git** — на git.zetit.ru (внутренний). Этот репо: `git@git.zetit.ru:zuevav/ZBrain.git`
|
||||||
|
- **gbrain** — vendored mirror на `git@git.zetit.ru:zuevav/gbrain-mirror.git`
|
||||||
|
- **Параллельные проекты** — ZetitConnect (тоже Node/React/Postgres), TeleraPharma инфра, личные проекты
|
||||||
|
- **Стиль работы** — детальные spec'и + поэтапная реализация через Claude Code
|
||||||
|
|
||||||
|
## Технологический стек
|
||||||
|
|
||||||
|
| Слой | Технология |
|
||||||
|
|------|-----------|
|
||||||
|
| Backend runtime | Node.js 22 LTS + TypeScript 5.6 |
|
||||||
|
| Backend framework | Express 4 |
|
||||||
|
| ORM | Drizzle ORM |
|
||||||
|
| Frontend | React 18 + Vite + TypeScript |
|
||||||
|
| Styling | Tailwind CSS + shadcn/ui |
|
||||||
|
| State | React Query + Zustand |
|
||||||
|
| Forms | React Hook Form + Zod |
|
||||||
|
| DB | PostgreSQL 16 + pgvector |
|
||||||
|
| Auth | passport.js (local + yandex + github) |
|
||||||
|
| Package manager | bun (workspaces) |
|
||||||
|
| Testing | Vitest |
|
||||||
|
| Deployment | Docker Compose + systemd для gbrain instances |
|
||||||
|
|
||||||
|
## Структура репозитория
|
||||||
|
|
||||||
|
```
|
||||||
|
ZBrain/
|
||||||
|
├── apps/
|
||||||
|
│ ├── api/ # Backend Express app
|
||||||
|
│ │ └── src/
|
||||||
|
│ │ ├── routes/ # HTTP routes
|
||||||
|
│ │ ├── services/ # бизнес-логика
|
||||||
|
│ │ ├── middleware/ # auth, RBAC, rate limit
|
||||||
|
│ │ ├── db/ # Drizzle schema и миграции
|
||||||
|
│ │ └── lib/ # утилиты
|
||||||
|
│ └── web/ # Frontend React app
|
||||||
|
│ └── src/
|
||||||
|
│ ├── pages/ # страницы (роуты)
|
||||||
|
│ ├── components/ # переиспользуемые компоненты
|
||||||
|
│ ├── hooks/ # React hooks
|
||||||
|
│ └── api/ # клиент к /api
|
||||||
|
├── packages/
|
||||||
|
│ ├── shared/ # типы и схемы общие для api+web
|
||||||
|
│ └── mcp-proxy/ # MCP proxy (валидация токенов, rate limit, audit)
|
||||||
|
├── deploy/ # Docker, systemd, nginx конфиги
|
||||||
|
├── docs/ # документация
|
||||||
|
└── scripts/ # bash скрипты для VM
|
||||||
|
```
|
||||||
|
|
||||||
|
## Конвенции кода
|
||||||
|
|
||||||
|
### TypeScript
|
||||||
|
|
||||||
|
- `strict: true`, `noUncheckedIndexedAccess: true`
|
||||||
|
- Все API ответы типизированы через `@zbrain/shared` types
|
||||||
|
- Все request bodies валидируются через Zod schemas из `@zbrain/shared`
|
||||||
|
- Никаких `any` без `// FIXME` комментария
|
||||||
|
|
||||||
|
### Express routes
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// apps/api/src/routes/brains.ts
|
||||||
|
import { Router } from 'express';
|
||||||
|
import { requireAuth, requireRole } from '../middleware/auth.js';
|
||||||
|
import { CreateBrainSchema } from '@zbrain/shared';
|
||||||
|
|
||||||
|
export const brainsRouter = Router();
|
||||||
|
|
||||||
|
brainsRouter.get('/', requireAuth, async (req, res) => {
|
||||||
|
const brains = await brainService.list(req.user);
|
||||||
|
res.json({ items: brains });
|
||||||
|
});
|
||||||
|
|
||||||
|
brainsRouter.post('/', requireAuth, requireRole(['owner', 'admin']), async (req, res) => {
|
||||||
|
const body = CreateBrainSchema.parse(req.body);
|
||||||
|
const brain = await brainService.create(body, req.user);
|
||||||
|
res.status(201).json(brain);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### React components
|
||||||
|
|
||||||
|
- Functional components с hooks (без class components)
|
||||||
|
- Props типизированы через interface
|
||||||
|
- Используем shadcn/ui компоненты, не пишем свои базовые (Button, Input)
|
||||||
|
- React Query для всех server-state, Zustand только для UI state
|
||||||
|
|
||||||
|
### Стиль ошибок
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// API возвращает структуру ApiError из @zbrain/shared
|
||||||
|
res.status(400).json({
|
||||||
|
error: 'Validation failed',
|
||||||
|
code: 'VALIDATION_ERROR',
|
||||||
|
details: zodError.flatten(),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Что точно делать НЕ надо
|
||||||
|
|
||||||
|
- **НЕ обращаться к gbrain напрямую из Claude Code** — только через brainhub-proxy
|
||||||
|
- **НЕ хранить токены в plain text** — всегда хеш SHA-256
|
||||||
|
- **НЕ логировать секреты** — пароли, токены, API ключи никогда не в logs
|
||||||
|
- **НЕ создавать прямые SQL запросы** — только через Drizzle ORM
|
||||||
|
- **НЕ забывать про audit log** для любых мутаций
|
||||||
|
- **НЕ забывать про RBAC** — каждый mutate endpoint проверяет роль
|
||||||
|
- **НЕ выставлять admin API на публичный порт 3010** — только /mcp и /oauth
|
||||||
|
|
||||||
|
## Что делать ВСЕГДА
|
||||||
|
|
||||||
|
- Перед началом нового спринта читать [docs/ROADMAP.md](docs/ROADMAP.md)
|
||||||
|
- Перед написанием нового API endpoint смотреть в [packages/shared/src/types.ts](packages/shared/src/types.ts) — может тип уже есть
|
||||||
|
- При изменении схемы БД — миграция через Drizzle, не вручную в Postgres
|
||||||
|
- При добавлении нового permission/role — обновлять [docs/SECURITY.md](docs/SECURITY.md)
|
||||||
|
- При нестандартном архитектурном решении — создавать ADR в [docs/DECISIONS/](docs/DECISIONS/)
|
||||||
|
- Закоммитить миграции и schema файлы вместе с кодом, который их использует
|
||||||
|
- Тесты для критичной логики (auth, MCP proxy, RBAC). UI можно без unit тестов в первой версии.
|
||||||
|
|
||||||
|
## Полезные команды
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Установка
|
||||||
|
bun install
|
||||||
|
|
||||||
|
# Локальная разработка (запустить Postgres сначала)
|
||||||
|
docker compose -f deploy/docker/docker-compose.dev.yml up -d
|
||||||
|
bun run db:migrate
|
||||||
|
bun run dev
|
||||||
|
|
||||||
|
# Миграции
|
||||||
|
bun run --cwd apps/api drizzle-kit generate
|
||||||
|
bun run db:migrate
|
||||||
|
|
||||||
|
# Запуск API + Web в watch mode
|
||||||
|
bun run dev
|
||||||
|
|
||||||
|
# Production build
|
||||||
|
bun run build
|
||||||
|
|
||||||
|
# Тесты
|
||||||
|
bun run test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Текущее состояние
|
||||||
|
|
||||||
|
> Обновляй этот раздел после каждого закрытого спринта.
|
||||||
|
|
||||||
|
- [x] **S0**: Инфраструктура VM (bootstrap скрипт готов)
|
||||||
|
- [ ] **S1**: Первый gbrain instance (create-brain.sh готов, нужно тестирование)
|
||||||
|
- [ ] **S2**: Brainhub каркас + локальный auth
|
||||||
|
- [ ] **S3**: MCP Proxy + Tokens
|
||||||
|
- [ ] **S4**: CRUD брейнов + источники + sync
|
||||||
|
- [ ] **S5**: Dashboard + Projects + Connect
|
||||||
|
- [ ] **S6**: OAuth + публичный контур
|
||||||
|
- [ ] **S7**: Hardening
|
||||||
|
|
||||||
|
## Связанные проекты
|
||||||
|
|
||||||
|
- **gbrain** (vendored) — https://github.com/garrytan/gbrain (mirror в git.zetit.ru/zuevav/gbrain-mirror)
|
||||||
|
- **ZetitConnect** — паттерны переиспользуем (Node/Express/React/Postgres стек)
|
||||||
|
- **ZetitClaude Code setup** — FNA proxy конфигурация, tmux/zsh wrappers
|
||||||
+162
@@ -0,0 +1,162 @@
|
|||||||
|
# INSTALL — Что делать после клонирования репозитория
|
||||||
|
|
||||||
|
Этот документ — единая точка входа после `git clone git@git.zetit.ru:zuevav/ZBrain.git`. Здесь только команды и checkpoints, без объяснений (объяснения — в `docs/`).
|
||||||
|
|
||||||
|
## Предусловия
|
||||||
|
|
||||||
|
- [ ] Виртуалка создана: **Ubuntu 22.04 LTS, 4 vCPU, 8 GB RAM, 80 GB SSD**
|
||||||
|
- [ ] SSH-доступ настроен
|
||||||
|
- [ ] Внутренний DNS `brain.zetit.local` указывает на эту VM
|
||||||
|
- [ ] (Опционально) Внешний DNS `brain.zetit.ru` указывает на edge-сервер или эту VM
|
||||||
|
- [ ] Создан **mirror gbrain** в твоём git:
|
||||||
|
```bash
|
||||||
|
# на любой машине с GitHub доступом, ОДИН РАЗ
|
||||||
|
git clone --mirror https://github.com/garrytan/gbrain.git
|
||||||
|
cd gbrain.git
|
||||||
|
git push --mirror git@git.zetit.ru:zuevav/gbrain-mirror.git
|
||||||
|
```
|
||||||
|
|
||||||
|
## Шаг 1 — Bootstrap VM
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# На VM
|
||||||
|
sudo mkdir -p /opt && sudo chown $USER:$USER /opt
|
||||||
|
git clone git@git.zetit.ru:zuevav/ZBrain.git /opt/zbrain
|
||||||
|
cd /opt/zbrain
|
||||||
|
|
||||||
|
# Запуск (правь параметры под себя!)
|
||||||
|
sudo TOTAL_RAM_GB=8 \
|
||||||
|
FNA_PROXY="http://client_001:ВСТАВЬ_ПАРОЛЬ@fna.zetit.ru:3128" \
|
||||||
|
GBRAIN_REPO="https://git.zetit.ru/zuevav/gbrain-mirror.git" \
|
||||||
|
GBRAIN_VERSION="master" \
|
||||||
|
bash scripts/bootstrap-vm.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Проверка:** последняя строчка вывода — "Bootstrap готов" + список Next Steps.
|
||||||
|
|
||||||
|
## Шаг 2 — Postgres пароли
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo -u postgres psql
|
||||||
|
```
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER USER postgres PASSWORD 'СГЕНЕРИРОВАТЬ_24_СИМВОЛА';
|
||||||
|
CREATE USER brainhub WITH PASSWORD 'ДРУГИЕ_24_СИМВОЛА';
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE brainhub TO brainhub;
|
||||||
|
\q
|
||||||
|
```
|
||||||
|
|
||||||
|
**Сохрани оба пароля в KeePass!**
|
||||||
|
|
||||||
|
## Шаг 3 — Заполнить `.env`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo cp /etc/zbrain/env.example /etc/zbrain/.env
|
||||||
|
sudo chmod 600 /etc/zbrain/.env
|
||||||
|
sudo chown zbrain:zbrain /etc/zbrain/.env
|
||||||
|
|
||||||
|
# Сгенерировать секреты
|
||||||
|
echo "SESSION_SECRET=$(openssl rand -hex 32)"
|
||||||
|
echo "JWT_SECRET=$(openssl rand -hex 32)"
|
||||||
|
echo "TOKEN_ENCRYPTION_KEY=$(openssl rand -hex 16)"
|
||||||
|
|
||||||
|
sudo nano /etc/zbrain/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
Заполнить:
|
||||||
|
- `DATABASE_URL` — с паролем brainhub из шага 2
|
||||||
|
- `HTTPS_PROXY`, `HTTP_PROXY` — с реальным паролем FNA
|
||||||
|
- `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`
|
||||||
|
- Секреты из openssl выше
|
||||||
|
- `INITIAL_OWNER_EMAIL`, `INITIAL_OWNER_PASSWORD`
|
||||||
|
|
||||||
|
## Шаг 4 — Первый брейн
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash /opt/zbrain/scripts/create-brain.sh zetit "ZETIT MSP" 3001
|
||||||
|
```
|
||||||
|
|
||||||
|
**Проверка:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl status zbrain-gbrain-zetit
|
||||||
|
curl -s http://localhost:3001/health || curl -s http://localhost:3001/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Шаг 5 — Тестовый импорт
|
||||||
|
|
||||||
|
Подготовь любую папку с markdown-файлами и:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo -u zbrain bash -c '
|
||||||
|
export PATH=$HOME/.bun/bin:$PATH
|
||||||
|
source /etc/zbrain/.env
|
||||||
|
export DATABASE_URL=$(jq -r .database_url /var/lib/zbrain/brains/zetit/config.json)
|
||||||
|
cd /var/lib/zbrain/gbrain
|
||||||
|
bun run gbrain import /tmp/test-markdown/
|
||||||
|
'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Проверка:**
|
||||||
|
|
||||||
|
```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;
|
||||||
|
'
|
||||||
|
```
|
||||||
|
|
||||||
|
Если `embedded < chunks` — embeddings ещё генерируются (проверь логи) или OPENAI_API_KEY не работает (проверь curl через FNA proxy).
|
||||||
|
|
||||||
|
## Шаг 6 — Опционально: подключить Claude Code напрямую
|
||||||
|
|
||||||
|
Пока brainhub не написан (Sprint 2+), можно подключиться к gbrain через SSH-туннель:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# С твоей машины
|
||||||
|
ssh -L 3001:127.0.0.1:3001 admin@<vm-ip>
|
||||||
|
```
|
||||||
|
|
||||||
|
В `~/.claude/mcp.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"zbrain-zetit-direct": {
|
||||||
|
"transport": "http",
|
||||||
|
"url": "http://127.0.0.1:3001"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Перезапустить Claude Code, проверить MCP инструменты.
|
||||||
|
|
||||||
|
После Sprint 2-3 эта прямая связь будет заменена на `https://brain.zetit.ru/mcp/zetit` с токеном.
|
||||||
|
|
||||||
|
## Что дальше
|
||||||
|
|
||||||
|
См. **[docs/ROADMAP.md](docs/ROADMAP.md)** — план по спринтам.
|
||||||
|
|
||||||
|
Следующая большая задача — **Sprint 2**: каркас brainhub с auth и UI. Это уже работа в коде:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/zbrain
|
||||||
|
claude # запустить Claude Code в репо
|
||||||
|
# В Claude Code:
|
||||||
|
# > Прочитай CLAUDE.md и docs/ROADMAP.md, начнём Sprint 2.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Если что-то пошло не так
|
||||||
|
|
||||||
|
См. **[docs/DEPLOYMENT.md](docs/DEPLOYMENT.md)** раздел "Типовые проблемы" и **[docs/OPERATIONS.md](docs/OPERATIONS.md)** раздел "Runbook'и инцидентов".
|
||||||
|
|
||||||
|
Логи везде:
|
||||||
|
- bootstrap: stdout скрипта + `/var/log/syslog`
|
||||||
|
- Postgres: `/var/log/postgresql/postgresql-16-main.log`
|
||||||
|
- gbrain: `journalctl -u zbrain-gbrain-<name>` и `/var/log/zbrain/<name>.log`
|
||||||
|
- brainhub (когда будет): `/var/log/zbrain/brainhub.log`
|
||||||
|
- nginx: `/var/log/nginx/zbrain-*.log`
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
Copyright (c) 2026 ZETIT (ИП Зуев Алексей Викторович). All rights reserved.
|
||||||
|
|
||||||
|
Proprietary and confidential.
|
||||||
|
|
||||||
|
Unauthorized copying of this repository, via any medium, is strictly prohibited
|
||||||
|
without express written permission from the copyright holder.
|
||||||
|
|
||||||
|
This software depends on third-party components, including:
|
||||||
|
- gbrain (https://github.com/garrytan/gbrain) — MIT License
|
||||||
|
- Other open-source libraries as declared in package.json files
|
||||||
|
|
||||||
|
These components retain their original licenses and copyright notices.
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
# ZBrain
|
||||||
|
|
||||||
|
Централизованная база знаний для AI-агентов ZETIT. Веб-админка + MCP-прокси поверх [gbrain](https://github.com/garrytan/gbrain).
|
||||||
|
|
||||||
|
## Зачем это
|
||||||
|
|
||||||
|
У нас много разных контекстов, и каждый Claude Code инстанс сейчас читает документацию с диска целиком при каждом запросе:
|
||||||
|
|
||||||
|
- **ZetitConnect MSP** — спецификации, спринты, архитектура
|
||||||
|
- **TeleraPharma** — runbook'и по Exchange, MikroTik, FortiGate, 1C
|
||||||
|
- **ZETIT клиенты** — конфигурации, инциденты, скрипты
|
||||||
|
- **Personal** — личные проекты, недвижимость, авто
|
||||||
|
- **Community (Смоленская 10)** — общедомовое, ЖКХ
|
||||||
|
|
||||||
|
ZBrain даёт единую точку доступа: гибридный семантический поиск по корпусу, MCP-интеграция с Claude Code/Cursor, разделение по доменам с изоляцией доступа.
|
||||||
|
|
||||||
|
## Архитектура
|
||||||
|
|
||||||
|
```
|
||||||
|
ИНТЕРНЕТ
|
||||||
|
│
|
||||||
|
↓
|
||||||
|
brain.zetit.ru (nginx, TLS, OAuth)
|
||||||
|
│
|
||||||
|
┌───────────────┴────────────────┐
|
||||||
|
│ brainhub-public │
|
||||||
|
│ - OAuth 2.1 (PKCE) │
|
||||||
|
│ - токены с scope │
|
||||||
|
│ - rate limit │
|
||||||
|
│ - audit log │
|
||||||
|
└───────────────┬────────────────┘
|
||||||
|
│
|
||||||
|
══════════════════════════════╪═══════════════════════════════
|
||||||
|
VPN / внутренний контур
|
||||||
|
│
|
||||||
|
┌───────────────┴────────────────┐
|
||||||
|
│ brainhub-internal │
|
||||||
|
│ - Web UI (React) │
|
||||||
|
│ - Admin API │
|
||||||
|
│ - MCP proxy router │
|
||||||
|
│ - SSE event aggregator │
|
||||||
|
└───────┬────────────┬───────────┘
|
||||||
|
│ │
|
||||||
|
┌───────┴────────────┴──────────┐
|
||||||
|
│ gbrain instances (HTTP) │
|
||||||
|
│ :3001 → gbrain_zetit │
|
||||||
|
│ :3002 → gbrain_telerapharma │
|
||||||
|
│ :3003 → gbrain_personal │
|
||||||
|
│ :3004 → gbrain_community │
|
||||||
|
└───────┬───────────────────────┘
|
||||||
|
│
|
||||||
|
┌───────┴───────────────────────┐
|
||||||
|
│ PostgreSQL 16 + pgvector │
|
||||||
|
│ - brainhub (admin/auth/audit)│
|
||||||
|
│ - gbrain_zetit │
|
||||||
|
│ - gbrain_telerapharma │
|
||||||
|
│ - gbrain_personal │
|
||||||
|
│ - gbrain_community │
|
||||||
|
└───────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Стек
|
||||||
|
|
||||||
|
- **Backend**: Node.js 22 LTS + TypeScript + Express
|
||||||
|
- **Frontend**: React 18 + TypeScript + Vite + Tailwind + shadcn/ui
|
||||||
|
- **DB**: PostgreSQL 16 + pgvector + pg_trgm
|
||||||
|
- **Auth**: Local email/password + Yandex OAuth + GitHub OAuth (passport.js)
|
||||||
|
- **Engine**: gbrain (vendored через git.zetit.ru/zuevav/gbrain-mirror)
|
||||||
|
- **Runtime**: Docker Compose в prod, native dev на разработке
|
||||||
|
- **Deploy**: VM Ubuntu 22.04 в сети ZETIT
|
||||||
|
|
||||||
|
## Структура репозитория
|
||||||
|
|
||||||
|
```
|
||||||
|
ZBrain/
|
||||||
|
├── apps/
|
||||||
|
│ ├── api/ # Express + TS backend
|
||||||
|
│ └── web/ # React + Vite frontend
|
||||||
|
├── packages/
|
||||||
|
│ ├── shared/ # общие типы и утилиты
|
||||||
|
│ └── mcp-proxy/ # MCP-прокси (валидация токенов, rate limit, audit)
|
||||||
|
├── deploy/
|
||||||
|
│ ├── docker/ # Dockerfile'ы и docker-compose
|
||||||
|
│ ├── systemd/ # unit-файлы для нативного деплоя
|
||||||
|
│ ├── nginx/ # конфиги reverse proxy
|
||||||
|
│ └── postgres/ # init scripts, tuning
|
||||||
|
├── docs/
|
||||||
|
│ ├── ARCHITECTURE.md # детальная архитектура
|
||||||
|
│ ├── DEPLOYMENT.md # инструкция по деплою
|
||||||
|
│ ├── SECURITY.md # модель безопасности
|
||||||
|
│ ├── API.md # API reference
|
||||||
|
│ ├── ROADMAP.md # план по спринтам
|
||||||
|
│ └── DECISIONS/ # ADR (Architecture Decision Records)
|
||||||
|
├── scripts/
|
||||||
|
│ ├── bootstrap-vm.sh # первичная установка на чистой Ubuntu 22.04
|
||||||
|
│ ├── create-brain.sh # создание нового gbrain instance
|
||||||
|
│ ├── backup.sh # бэкап всех БД
|
||||||
|
│ └── restore.sh # восстановление
|
||||||
|
└── README.md # этот файл
|
||||||
|
```
|
||||||
|
|
||||||
|
## Быстрый старт
|
||||||
|
|
||||||
|
### Развёртывание production VM
|
||||||
|
|
||||||
|
См. [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md). Кратко:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Создать VM (4 vCPU / 8 GB RAM / 80 GB SSD / Ubuntu 22.04)
|
||||||
|
# 2. Склонировать репо
|
||||||
|
git clone git@git.zetit.ru:zuevav/ZBrain.git /opt/zbrain
|
||||||
|
cd /opt/zbrain
|
||||||
|
|
||||||
|
# 3. Запустить bootstrap (ставит Postgres, Node, Bun, gbrain, тюнит ОС)
|
||||||
|
sudo bash scripts/bootstrap-vm.sh
|
||||||
|
|
||||||
|
# 4. Создать первый брейн
|
||||||
|
sudo bash scripts/create-brain.sh zetit "ZETIT MSP" 3001
|
||||||
|
|
||||||
|
# 5. Сконфигурировать .env (см. .env.example)
|
||||||
|
sudo nano /etc/zbrain/.env
|
||||||
|
|
||||||
|
# 6. Запустить brainhub
|
||||||
|
cd /opt/zbrain
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Локальная разработка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Зависимости
|
||||||
|
bun install
|
||||||
|
|
||||||
|
# Postgres в docker
|
||||||
|
docker compose -f deploy/docker/docker-compose.dev.yml up -d
|
||||||
|
|
||||||
|
# Миграции
|
||||||
|
bun run db:migrate
|
||||||
|
|
||||||
|
# Запуск API + Web
|
||||||
|
bun run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
API на http://localhost:3000, Web на http://localhost:5173.
|
||||||
|
|
||||||
|
## Дорожная карта
|
||||||
|
|
||||||
|
Подробности — в [docs/ROADMAP.md](docs/ROADMAP.md). Кратко по спринтам:
|
||||||
|
|
||||||
|
- **S0** — Инфраструктура VM, Postgres, базовая gbrain установка
|
||||||
|
- **S1** — Первый рабочий gbrain через `gbrain serve --http`
|
||||||
|
- **S2** — Каркас brainhub + локальный auth + RBAC
|
||||||
|
- **S3** — MCP-прокси + токены с scope
|
||||||
|
- **S4** — CRUD брейнов + источники + sync
|
||||||
|
- **S5** — Dashboard + Projects + Connect страница
|
||||||
|
- **S6** — OAuth (Yandex + GitHub) + публичный контур
|
||||||
|
- **S7** — Audit, 2FA, backup, метрики, hardening
|
||||||
|
|
||||||
|
## Безопасность
|
||||||
|
|
||||||
|
- Каждый gbrain instance имеет **отдельного Postgres-пользователя** и **отдельную БД**
|
||||||
|
- MCP-токены имеют **scope** (`mcp:read:<brain>`, `mcp:write:<brain>`, `mcp:admin:<brain>`)
|
||||||
|
- Все MCP-запросы проходят через **brainhub-proxy**, не напрямую к gbrain
|
||||||
|
- Все действия пишутся в **audit log**
|
||||||
|
- Публичный контур принимает **только** `/mcp/*` и `/oauth/*`
|
||||||
|
|
||||||
|
Подробности — [docs/SECURITY.md](docs/SECURITY.md).
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
Proprietary. © ZETIT, Зуев А.В.
|
||||||
|
|
||||||
|
gbrain используется по лицензии MIT, см. [гigit.zetit.ru/zuevav/gbrain-mirror](https://git.zetit.ru/zuevav/gbrain-mirror).
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"name": "@zbrain/api",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bun run --watch src/main.ts",
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/main.js",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"lint": "eslint src --ext .ts",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest",
|
||||||
|
"db:migrate": "bun run src/db/migrate.ts",
|
||||||
|
"db:seed": "bun run src/db/seed.ts",
|
||||||
|
"db:studio": "drizzle-kit studio"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@zbrain/shared": "workspace:*",
|
||||||
|
"@zbrain/mcp-proxy": "workspace:*",
|
||||||
|
"express": "^4.21.0",
|
||||||
|
"cookie-parser": "^1.4.6",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"helmet": "^8.0.0",
|
||||||
|
"compression": "^1.7.4",
|
||||||
|
"express-rate-limit": "^7.4.0",
|
||||||
|
"passport": "^0.7.0",
|
||||||
|
"passport-local": "^1.0.0",
|
||||||
|
"passport-yandex": "^0.0.5",
|
||||||
|
"passport-github2": "^0.1.12",
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"drizzle-orm": "^0.34.0",
|
||||||
|
"postgres": "^3.4.4",
|
||||||
|
"zod": "^3.23.8",
|
||||||
|
"pino": "^9.4.0",
|
||||||
|
"pino-http": "^10.3.0",
|
||||||
|
"otplib": "^12.0.1",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
|
"http-proxy-middleware": "^3.0.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/cookie-parser": "^1.4.7",
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
|
"@types/compression": "^1.7.5",
|
||||||
|
"@types/passport": "^1.0.16",
|
||||||
|
"@types/passport-local": "^1.0.38",
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
|
"@types/jsonwebtoken": "^9.0.7",
|
||||||
|
"@types/qrcode": "^1.5.5",
|
||||||
|
"drizzle-kit": "^0.26.0",
|
||||||
|
"vitest": "^2.1.0",
|
||||||
|
"tsx": "^4.19.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"declaration": false,
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@zbrain/shared": ["../../packages/shared/src/index.ts"],
|
||||||
|
"@zbrain/shared/*": ["../../packages/shared/src/*"],
|
||||||
|
"@zbrain/mcp-proxy": ["../../packages/mcp-proxy/src/index.ts"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"name": "@zbrain/web",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"lint": "eslint src --ext .ts,.tsx",
|
||||||
|
"test": "vitest run"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@zbrain/shared": "workspace:*",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-router-dom": "^6.27.0",
|
||||||
|
"@tanstack/react-query": "^5.59.0",
|
||||||
|
"axios": "^1.7.7",
|
||||||
|
"zustand": "^5.0.0",
|
||||||
|
"react-hook-form": "^7.53.0",
|
||||||
|
"zod": "^3.23.8",
|
||||||
|
"@hookform/resolvers": "^3.9.0",
|
||||||
|
"tailwindcss": "^3.4.13",
|
||||||
|
"lucide-react": "^0.451.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"tailwind-merge": "^2.5.4",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
|
"recharts": "^2.13.0",
|
||||||
|
"qrcode.react": "^4.0.1",
|
||||||
|
"react-hot-toast": "^2.4.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.3.11",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.2",
|
||||||
|
"vite": "^5.4.8",
|
||||||
|
"typescript": "^5.6.0",
|
||||||
|
"vitest": "^2.1.0",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"postcss": "^8.4.47",
|
||||||
|
"eslint": "^9.12.0",
|
||||||
|
"eslint-plugin-react": "^7.37.1",
|
||||||
|
"eslint-plugin-react-hooks": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"],
|
||||||
|
"@zbrain/shared": ["../../packages/shared/src/index.ts"],
|
||||||
|
"@zbrain/shared/*": ["../../packages/shared/src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# ZBrain Development Docker Compose
|
||||||
|
#
|
||||||
|
# Только Postgres - всё остальное запускается нативно через bun/node.
|
||||||
|
#
|
||||||
|
# Запуск:
|
||||||
|
# docker compose -f deploy/docker/docker-compose.dev.yml up -d
|
||||||
|
#
|
||||||
|
# Подключение:
|
||||||
|
# psql postgresql://zbrain:dev@localhost:5432/brainhub_dev
|
||||||
|
|
||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: pgvector/pgvector:pg16
|
||||||
|
container_name: zbrain-postgres-dev
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: zbrain
|
||||||
|
POSTGRES_PASSWORD: dev
|
||||||
|
POSTGRES_DB: brainhub_dev
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:5432:5432"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- postgres-dev-data:/var/lib/postgresql/data
|
||||||
|
- ./init-dev.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
||||||
|
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U zbrain"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# Опционально - adminer для просмотра БД через браузер
|
||||||
|
adminer:
|
||||||
|
image: adminer:latest
|
||||||
|
container_name: zbrain-adminer-dev
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8081:8080"
|
||||||
|
environment:
|
||||||
|
ADMINER_DEFAULT_SERVER: postgres
|
||||||
|
ADMINER_DESIGN: pepa-linha-dark
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres-dev-data:
|
||||||
|
driver: local
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# ZBrain Production Docker Compose
|
||||||
|
#
|
||||||
|
# Запуск:
|
||||||
|
# docker compose -f deploy/docker/docker-compose.yml up -d
|
||||||
|
#
|
||||||
|
# Postgres работает на хосте (не в контейнере) - так проще делать
|
||||||
|
# нативный pg_dump, тюнинг и обновления без даунтайма всех брейнов.
|
||||||
|
# gbrain instances тоже работают через systemd на хосте.
|
||||||
|
# В контейнерах - только brainhub (api + web) и nginx.
|
||||||
|
|
||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
# ============================================================
|
||||||
|
# Brainhub API (Node.js + Express)
|
||||||
|
# ============================================================
|
||||||
|
brainhub-api:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: deploy/docker/api.Dockerfile
|
||||||
|
image: zbrain/brainhub-api:latest
|
||||||
|
container_name: zbrain-api
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# Postgres на хосте, поэтому используем host network для api
|
||||||
|
# ИЛИ - оставляем bridge и в DATABASE_URL пишем host.docker.internal
|
||||||
|
network_mode: host
|
||||||
|
|
||||||
|
env_file:
|
||||||
|
- /etc/zbrain/.env
|
||||||
|
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
LOG_FILE: /var/log/zbrain/brainhub.log
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- /var/log/zbrain:/var/log/zbrain
|
||||||
|
- /etc/zbrain:/etc/zbrain:ro
|
||||||
|
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 30s
|
||||||
|
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "5"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Brainhub Web (статика через nginx внутри контейнера)
|
||||||
|
# ============================================================
|
||||||
|
brainhub-web:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: deploy/docker/web.Dockerfile
|
||||||
|
image: zbrain/brainhub-web:latest
|
||||||
|
container_name: zbrain-web
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8080:80" # только localhost, внешний доступ через системный nginx
|
||||||
|
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "5"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Networks
|
||||||
|
# ============================================================
|
||||||
|
# brainhub-api использует host network для прямого доступа к Postgres
|
||||||
|
# и к gbrain instances (localhost:3001-3099).
|
||||||
|
# brainhub-web стоит на bridge с port forward на 127.0.0.1:8080.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- ZBrain Dev Postgres Init
|
||||||
|
-- Создаёт расширения и тестовые БД для локальной разработки
|
||||||
|
|
||||||
|
CREATE EXTENSION IF NOT EXISTS vector;
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
|
|
||||||
|
-- Тестовая БД для gbrain
|
||||||
|
CREATE DATABASE gbrain_dev OWNER zbrain;
|
||||||
|
\c gbrain_dev
|
||||||
|
CREATE EXTENSION IF NOT EXISTS vector;
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||||
|
|
||||||
|
\c brainhub_dev
|
||||||
|
-- В brainhub_dev расширения уже созданы выше, миграции прокатятся через приложение
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
# ZBrain Nginx Configuration
|
||||||
|
# ===========================
|
||||||
|
# Два server-блока:
|
||||||
|
# 1. Внутренний (brain.zetit.local) - полный доступ к UI и API
|
||||||
|
# 2. Публичный (brain.zetit.ru) - только /mcp/* и /oauth/*
|
||||||
|
#
|
||||||
|
# Положить в /etc/nginx/sites-available/zbrain
|
||||||
|
# и сделать symlink в /etc/nginx/sites-enabled/
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Upstream'ы
|
||||||
|
# ============================================================
|
||||||
|
upstream brainhub_api {
|
||||||
|
server 127.0.0.1:3000;
|
||||||
|
keepalive 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream brainhub_api_public {
|
||||||
|
server 127.0.0.1:3010;
|
||||||
|
keepalive 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream brainhub_web {
|
||||||
|
server 127.0.0.1:8080;
|
||||||
|
keepalive 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Rate limiting для публичного MCP
|
||||||
|
limit_req_zone $binary_remote_addr zone=mcp_public:10m rate=30r/s;
|
||||||
|
limit_req_zone $http_authorization zone=mcp_token:10m rate=100r/s;
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Внутренний контур: brain.zetit.local
|
||||||
|
# ============================================================
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name brain.zetit.local;
|
||||||
|
|
||||||
|
# Внутри сети можно без HTTPS, либо self-signed cert
|
||||||
|
# Если нужен HTTPS - раскомментировать listen 443 ниже и убрать listen 80
|
||||||
|
|
||||||
|
access_log /var/log/nginx/zbrain-internal.access.log;
|
||||||
|
error_log /var/log/nginx/zbrain-internal.error.log;
|
||||||
|
|
||||||
|
client_max_body_size 50M;
|
||||||
|
|
||||||
|
# Web UI (статика)
|
||||||
|
location / {
|
||||||
|
proxy_pass http://brainhub_web;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
# API
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://brainhub_api;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
|
||||||
|
# Таймауты для долгих операций (sync, import)
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
proxy_send_timeout 300s;
|
||||||
|
}
|
||||||
|
|
||||||
|
# SSE events
|
||||||
|
location /api/events {
|
||||||
|
proxy_pass http://brainhub_api;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_cache off;
|
||||||
|
proxy_read_timeout 24h;
|
||||||
|
|
||||||
|
# SSE headers
|
||||||
|
chunked_transfer_encoding off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# MCP - внутренний доступ (тоже с токенами, но без rate limit)
|
||||||
|
location /mcp/ {
|
||||||
|
proxy_pass http://brainhub_api;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header Authorization $http_authorization;
|
||||||
|
|
||||||
|
proxy_read_timeout 120s;
|
||||||
|
proxy_buffering off;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Публичный контур: brain.zetit.ru
|
||||||
|
# ============================================================
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name brain.zetit.ru;
|
||||||
|
|
||||||
|
# HTTP -> HTTPS redirect (кроме ACME challenge)
|
||||||
|
location /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/certbot;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
listen [::]:443 ssl http2;
|
||||||
|
server_name brain.zetit.ru;
|
||||||
|
|
||||||
|
# TLS
|
||||||
|
ssl_certificate /etc/letsencrypt/live/brain.zetit.ru/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/brain.zetit.ru/privkey.pem;
|
||||||
|
ssl_session_timeout 1d;
|
||||||
|
ssl_session_cache shared:MozSSL:10m;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_prefer_server_ciphers off;
|
||||||
|
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
|
||||||
|
|
||||||
|
# HSTS
|
||||||
|
add_header Strict-Transport-Security "max-age=63072000" always;
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-Frame-Options "DENY" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
|
||||||
|
access_log /var/log/nginx/zbrain-public.access.log;
|
||||||
|
error_log /var/log/nginx/zbrain-public.error.log;
|
||||||
|
|
||||||
|
client_max_body_size 10M;
|
||||||
|
|
||||||
|
# ВАЖНО: на публичном контуре доступны только /mcp/* и /oauth/*
|
||||||
|
# Никакого UI, API админки и т.д.
|
||||||
|
|
||||||
|
# MCP proxy с rate limiting
|
||||||
|
location /mcp/ {
|
||||||
|
# Лимит по IP - защита от подбора токенов
|
||||||
|
limit_req zone=mcp_public burst=60 nodelay;
|
||||||
|
# Лимит по токену - защита от взбесившегося скрипта
|
||||||
|
limit_req zone=mcp_token burst=200 nodelay;
|
||||||
|
limit_req_status 429;
|
||||||
|
|
||||||
|
proxy_pass http://brainhub_api_public;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header Authorization $http_authorization;
|
||||||
|
|
||||||
|
proxy_read_timeout 120s;
|
||||||
|
proxy_buffering off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# OAuth flow для удалённой авторизации
|
||||||
|
location /oauth/ {
|
||||||
|
limit_req zone=mcp_public burst=20 nodelay;
|
||||||
|
|
||||||
|
proxy_pass http://brainhub_api_public;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Health check (без rate limit, для мониторинга)
|
||||||
|
location /health {
|
||||||
|
proxy_pass http://brainhub_api_public/health;
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Всё остальное - 404, чтобы публично ничего лишнего не светить
|
||||||
|
location / {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "zbrain",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Централизованная база знаний для AI-агентов ZETIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git@git.zetit.ru:zuevav/ZBrain.git"
|
||||||
|
},
|
||||||
|
"author": "Зуев Алексей Викторович <zuev@zetit.ru>",
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"workspaces": [
|
||||||
|
"apps/*",
|
||||||
|
"packages/*"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bun run --filter '*' dev",
|
||||||
|
"build": "bun run --filter '*' build",
|
||||||
|
"lint": "bun run --filter '*' lint",
|
||||||
|
"test": "bun run --filter '*' test",
|
||||||
|
"typecheck": "bun run --filter '*' typecheck",
|
||||||
|
"db:migrate": "bun run --cwd apps/api db:migrate",
|
||||||
|
"db:seed": "bun run --cwd apps/api db:seed",
|
||||||
|
"clean": "rm -rf node_modules apps/*/node_modules apps/*/dist packages/*/node_modules packages/*/dist"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.0.0",
|
||||||
|
"typescript": "^5.5.0",
|
||||||
|
"prettier": "^3.3.0",
|
||||||
|
"eslint": "^9.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.0.0",
|
||||||
|
"bun": ">=1.2.0"
|
||||||
|
},
|
||||||
|
"packageManager": "bun@1.2.0"
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "@zbrain/shared",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "./src/index.ts",
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts",
|
||||||
|
"./types": "./src/types.ts",
|
||||||
|
"./schemas": "./src/schemas.ts"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"zod": "^3.23.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './types.js';
|
||||||
|
export * from './schemas.js';
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const UserRoleSchema = z.enum(['owner', 'admin', 'editor', 'viewer']);
|
||||||
|
|
||||||
|
export const LoginRequestSchema = z.object({
|
||||||
|
email: z.string().email(),
|
||||||
|
password: z.string().min(8).max(256),
|
||||||
|
totpCode: z.string().regex(/^\d{6}$/).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateBrainSchema = z.object({
|
||||||
|
name: z.string().regex(/^[a-z0-9_]{1,32}$/, 'Только [a-z0-9_], до 32 символов'),
|
||||||
|
displayName: z.string().min(1).max(128),
|
||||||
|
description: z.string().max(1024).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const MCPScopeSchema = z.string().regex(
|
||||||
|
/^mcp:(read|write|admin):[a-z0-9_*]{1,32}$/,
|
||||||
|
'Формат: mcp:<read|write|admin>:<brain-name>'
|
||||||
|
) as z.ZodType<`mcp:${'read' | 'write' | 'admin'}:${string}`>;
|
||||||
|
|
||||||
|
export const CreateTokenSchema = z.object({
|
||||||
|
name: z.string().min(1).max(128),
|
||||||
|
scopes: z.array(MCPScopeSchema).min(1).max(20),
|
||||||
|
ipAllowlist: z.array(z.string()).max(50).optional(),
|
||||||
|
expiresInDays: z.number().int().min(1).max(3650).nullable().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SourceConfigGitSchema = z.object({
|
||||||
|
type: z.literal('git'),
|
||||||
|
url: z.string().url(),
|
||||||
|
branch: z.string().default('main'),
|
||||||
|
subpath: z.string().optional(),
|
||||||
|
localPath: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SourceConfigLocalDirSchema = z.object({
|
||||||
|
type: z.literal('local_dir'),
|
||||||
|
path: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SourceConfigManualSchema = z.object({
|
||||||
|
type: z.literal('manual'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SourceConfigSchema = z.discriminatedUnion('type', [
|
||||||
|
SourceConfigGitSchema,
|
||||||
|
SourceConfigLocalDirSchema,
|
||||||
|
SourceConfigManualSchema,
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const CreateSourceSchema = z.object({
|
||||||
|
type: z.enum(['git', 'local_dir', 'manual']),
|
||||||
|
config: SourceConfigSchema,
|
||||||
|
schedule: z.string().nullable().optional(), // cron expression
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateProjectSchema = z.object({
|
||||||
|
name: z.string().min(1).max(128),
|
||||||
|
slug: z.string().regex(/^[a-z0-9-]{1,64}$/, 'lowercase, цифры, дефисы'),
|
||||||
|
repoUrl: z.string().url().nullable().optional(),
|
||||||
|
description: z.string().max(1024).nullable().optional(),
|
||||||
|
});
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
// Shared types - используются и в API, и в Web
|
||||||
|
// Источник правды для контракта между frontend и backend
|
||||||
|
|
||||||
|
export type UserRole = 'owner' | 'admin' | 'editor' | 'viewer';
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
displayName: string | null;
|
||||||
|
role: UserRole;
|
||||||
|
oauthProvider: 'yandex' | 'github' | null;
|
||||||
|
totpEnabled: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
lastLoginAt: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Brain {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
displayName: string;
|
||||||
|
description: string | null;
|
||||||
|
postgresDatabase: string;
|
||||||
|
mcpInternalPort: number;
|
||||||
|
ownerUserId: string;
|
||||||
|
createdAt: string;
|
||||||
|
|
||||||
|
// Computed stats
|
||||||
|
stats?: BrainStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BrainStats {
|
||||||
|
pageCount: number;
|
||||||
|
chunkCount: number;
|
||||||
|
embeddedCount: number;
|
||||||
|
linkCount: number;
|
||||||
|
diskSizeMB: number;
|
||||||
|
lastSyncAt: string | null;
|
||||||
|
healthStatus: 'healthy' | 'warning' | 'critical';
|
||||||
|
embeddingCoverage: number; // 0..1
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SourceType = 'git' | 'local_dir' | 'manual';
|
||||||
|
|
||||||
|
export interface Source {
|
||||||
|
id: string;
|
||||||
|
brainId: string;
|
||||||
|
type: SourceType;
|
||||||
|
config: SourceConfig;
|
||||||
|
schedule: string | null;
|
||||||
|
lastSyncAt: string | null;
|
||||||
|
lastSyncStatus: SyncStatus | null;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SourceConfig =
|
||||||
|
| { type: 'git'; url: string; branch: string; subpath?: string; localPath: string }
|
||||||
|
| { type: 'local_dir'; path: string }
|
||||||
|
| { type: 'manual' };
|
||||||
|
|
||||||
|
export type SyncStatus = 'idle' | 'running' | 'success' | 'failed';
|
||||||
|
|
||||||
|
export type MCPScope = `mcp:${'read' | 'write' | 'admin'}:${string}`;
|
||||||
|
|
||||||
|
export interface AccessToken {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
prefix: string;
|
||||||
|
createdByUserId: string;
|
||||||
|
scopes: MCPScope[];
|
||||||
|
ipAllowlist: string[] | null;
|
||||||
|
createdAt: string;
|
||||||
|
expiresAt: string | null;
|
||||||
|
lastUsedAt: string | null;
|
||||||
|
lastUsedIp: string | null;
|
||||||
|
revokedAt: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateTokenRequest {
|
||||||
|
name: string;
|
||||||
|
scopes: MCPScope[];
|
||||||
|
ipAllowlist?: string[];
|
||||||
|
expiresInDays?: number; // null = no expiration
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateTokenResponse {
|
||||||
|
token: AccessToken;
|
||||||
|
plaintext: string; // показывается ОДИН РАЗ
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Project {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
repoUrl: string | null;
|
||||||
|
description: string | null;
|
||||||
|
ownerUserId: string;
|
||||||
|
createdAt: string;
|
||||||
|
connections: ProjectConnection[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProjectConnection {
|
||||||
|
brainId: string;
|
||||||
|
brainName: string;
|
||||||
|
brainDisplayName: string;
|
||||||
|
tokenId: string;
|
||||||
|
tokenName: string;
|
||||||
|
scope: 'read' | 'write';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConnectSnippet {
|
||||||
|
client: 'claude-code' | 'cursor' | 'cli';
|
||||||
|
language: string;
|
||||||
|
filename: string | null;
|
||||||
|
content: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuditEntry {
|
||||||
|
id: number;
|
||||||
|
actorType: 'user' | 'token';
|
||||||
|
actorId: string;
|
||||||
|
actorName: string;
|
||||||
|
action: string;
|
||||||
|
resourceType: string | null;
|
||||||
|
resourceId: string | null;
|
||||||
|
payload: Record<string, unknown> | null;
|
||||||
|
ip: string | null;
|
||||||
|
userAgent: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API responses
|
||||||
|
|
||||||
|
export interface PaginatedResponse<T> {
|
||||||
|
items: T[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiError {
|
||||||
|
error: string;
|
||||||
|
code: string;
|
||||||
|
details?: Record<string, unknown>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"declaration": true,
|
||||||
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"verbatimModuleSyntax": false,
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
Executable
+158
@@ -0,0 +1,158 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# ZBrain Backup
|
||||||
|
# ==============
|
||||||
|
# Делает дамп всех Postgres БД (brainhub + все gbrain_*) и конфигов.
|
||||||
|
# Шифрует архивы через age (нужен age и публичный ключ).
|
||||||
|
#
|
||||||
|
# Запуск из cron, например:
|
||||||
|
# 0 3 * * * root /opt/zbrain/scripts/backup.sh
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Конфигурация
|
||||||
|
# ============================================================
|
||||||
|
BACKUP_DIR="${BACKUP_DIR:-/var/backups/zbrain}"
|
||||||
|
RETENTION_DAYS="${RETENTION_DAYS:-14}"
|
||||||
|
AGE_RECIPIENT_FILE="${AGE_RECIPIENT_FILE:-/etc/zbrain/backup.age.pub}"
|
||||||
|
LOG_FILE="${LOG_FILE:-/var/log/zbrain/backup.log}"
|
||||||
|
ZBRAIN_CONFIG_DIR="/etc/zbrain"
|
||||||
|
ZBRAIN_DATA_DIR="/var/lib/zbrain"
|
||||||
|
|
||||||
|
DATE=$(date +%Y%m%d-%H%M%S)
|
||||||
|
BACKUP_PATH="${BACKUP_DIR}/${DATE}"
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
err() {
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $*" | tee -a "$LOG_FILE" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Проверки
|
||||||
|
# ============================================================
|
||||||
|
[[ $EUID -eq 0 ]] || { err "Требуется root"; exit 1; }
|
||||||
|
|
||||||
|
if ! command -v age &>/dev/null; then
|
||||||
|
err "age не установлен. apt install age"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$AGE_RECIPIENT_FILE" ]]; then
|
||||||
|
err "Файл получателя age не найден: $AGE_RECIPIENT_FILE"
|
||||||
|
err "Создай ключ: age-keygen -o /etc/zbrain/backup.age"
|
||||||
|
err "И публичную часть в $AGE_RECIPIENT_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$BACKUP_PATH" "$(dirname "$LOG_FILE")"
|
||||||
|
chmod 700 "$BACKUP_DIR"
|
||||||
|
|
||||||
|
log "=== ZBrain backup: $DATE ==="
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Postgres dump (каждая БД отдельно)
|
||||||
|
# ============================================================
|
||||||
|
log "Дамп Postgres БД..."
|
||||||
|
|
||||||
|
# Список всех ZBrain БД (brainhub + gbrain_*)
|
||||||
|
DATABASES=$(sudo -u postgres psql -tAc "
|
||||||
|
SELECT datname FROM pg_database
|
||||||
|
WHERE datname = 'brainhub' OR datname LIKE 'gbrain_%'
|
||||||
|
ORDER BY datname
|
||||||
|
")
|
||||||
|
|
||||||
|
for db in $DATABASES; do
|
||||||
|
log " ${db}"
|
||||||
|
dump_file="${BACKUP_PATH}/${db}.sql"
|
||||||
|
sudo -u postgres pg_dump --format=custom --compress=9 --file="${dump_file}.dump" "$db"
|
||||||
|
|
||||||
|
# Шифруем
|
||||||
|
age -R "$AGE_RECIPIENT_FILE" -o "${dump_file}.dump.age" "${dump_file}.dump"
|
||||||
|
rm "${dump_file}.dump"
|
||||||
|
log " ✓ ${db}.sql.dump.age ($(du -h "${dump_file}.dump.age" | cut -f1))"
|
||||||
|
done
|
||||||
|
|
||||||
|
# pg_dumpall для глобальных объектов (roles, tablespaces)
|
||||||
|
log "Дамп глобальных Postgres объектов..."
|
||||||
|
sudo -u postgres pg_dumpall --globals-only > "${BACKUP_PATH}/_globals.sql"
|
||||||
|
age -R "$AGE_RECIPIENT_FILE" -o "${BACKUP_PATH}/_globals.sql.age" "${BACKUP_PATH}/_globals.sql"
|
||||||
|
rm "${BACKUP_PATH}/_globals.sql"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Конфиги
|
||||||
|
# ============================================================
|
||||||
|
log "Архив конфигов..."
|
||||||
|
|
||||||
|
tar czf "${BACKUP_PATH}/config.tar.gz" \
|
||||||
|
-C / \
|
||||||
|
etc/zbrain \
|
||||||
|
etc/postgresql/16/main/postgresql.conf \
|
||||||
|
etc/postgresql/16/main/conf.d \
|
||||||
|
etc/postgresql/16/main/pg_hba.conf \
|
||||||
|
etc/systemd/system/zbrain-*.service \
|
||||||
|
etc/nginx 2>/dev/null || true
|
||||||
|
|
||||||
|
age -R "$AGE_RECIPIENT_FILE" -o "${BACKUP_PATH}/config.tar.gz.age" "${BACKUP_PATH}/config.tar.gz"
|
||||||
|
rm "${BACKUP_PATH}/config.tar.gz"
|
||||||
|
|
||||||
|
log "✓ config.tar.gz.age"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# gbrain data dir (config.json per-brain содержит пароли)
|
||||||
|
# ============================================================
|
||||||
|
log "Архив brain configs..."
|
||||||
|
|
||||||
|
if [[ -d "$ZBRAIN_DATA_DIR/brains" ]]; then
|
||||||
|
tar czf "${BACKUP_PATH}/brains-meta.tar.gz" \
|
||||||
|
-C "$ZBRAIN_DATA_DIR" \
|
||||||
|
--exclude='brains/*/data' \
|
||||||
|
brains/
|
||||||
|
age -R "$AGE_RECIPIENT_FILE" -o "${BACKUP_PATH}/brains-meta.tar.gz.age" "${BACKUP_PATH}/brains-meta.tar.gz"
|
||||||
|
rm "${BACKUP_PATH}/brains-meta.tar.gz"
|
||||||
|
log "✓ brains-meta.tar.gz.age"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Манифест
|
||||||
|
# ============================================================
|
||||||
|
cat > "${BACKUP_PATH}/MANIFEST.txt" <<EOF
|
||||||
|
ZBrain Backup
|
||||||
|
=============
|
||||||
|
Date: $(date -Iseconds)
|
||||||
|
Hostname: $(hostname -f)
|
||||||
|
Postgres version: $(sudo -u postgres psql -tAc 'SHOW server_version')
|
||||||
|
|
||||||
|
Databases:
|
||||||
|
$(echo "$DATABASES" | sed 's/^/ - /')
|
||||||
|
|
||||||
|
Files:
|
||||||
|
$(ls -lah "$BACKUP_PATH" | tail -n +2 | awk '{print " - " $NF " (" $5 ")"}')
|
||||||
|
|
||||||
|
Восстановление:
|
||||||
|
bash /opt/zbrain/scripts/restore.sh ${DATE}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Размер итогового бэкапа
|
||||||
|
# ============================================================
|
||||||
|
TOTAL_SIZE=$(du -sh "$BACKUP_PATH" | cut -f1)
|
||||||
|
log "✓ Backup завершён: $BACKUP_PATH ($TOTAL_SIZE)"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Retention - удаляем старые бэкапы
|
||||||
|
# ============================================================
|
||||||
|
log "Чистка бэкапов старше $RETENTION_DAYS дней..."
|
||||||
|
find "$BACKUP_DIR" -maxdepth 1 -type d -name '????????-??????' -mtime +$RETENTION_DAYS -exec rm -rf {} \;
|
||||||
|
log "✓ Чистка завершена"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# (опционально) push на удалённое хранилище
|
||||||
|
# ============================================================
|
||||||
|
# rsync -avz "$BACKUP_PATH" backup@remote-host:/backups/zbrain/
|
||||||
|
# или rclone copy "$BACKUP_PATH" "remote:zbrain-backups/${DATE}"
|
||||||
|
|
||||||
|
log "=== Готово ==="
|
||||||
Executable
+485
@@ -0,0 +1,485 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# ZBrain VM Bootstrap
|
||||||
|
# ====================
|
||||||
|
# Подготавливает чистую Ubuntu 22.04 VM к работе как ZBrain хост.
|
||||||
|
# Устанавливает: Postgres 16 + pgvector, Node 22 LTS, Bun, Docker, gbrain.
|
||||||
|
#
|
||||||
|
# Использование:
|
||||||
|
# sudo bash scripts/bootstrap-vm.sh
|
||||||
|
#
|
||||||
|
# Предполагается: запуск от root на свежей Ubuntu 22.04 LTS.
|
||||||
|
# Скрипт идемпотентный - можно перезапускать.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Конфигурация - правь под себя ПЕРЕД запуском
|
||||||
|
# ============================================================
|
||||||
|
ZBRAIN_USER="${ZBRAIN_USER:-zbrain}"
|
||||||
|
ZBRAIN_HOME="/opt/zbrain"
|
||||||
|
ZBRAIN_CONFIG_DIR="/etc/zbrain"
|
||||||
|
ZBRAIN_DATA_DIR="/var/lib/zbrain"
|
||||||
|
ZBRAIN_LOG_DIR="/var/log/zbrain"
|
||||||
|
|
||||||
|
# FNA-прокси для исходящих запросов к OpenAI/Anthropic
|
||||||
|
FNA_PROXY="${FNA_PROXY:-http://client_001:PASSWORD@fna.zetit.ru:3128}"
|
||||||
|
|
||||||
|
POSTGRES_VERSION="16"
|
||||||
|
NODE_VERSION="22"
|
||||||
|
GBRAIN_VERSION="${GBRAIN_VERSION:-master}" # потом закрепим тег после первого деплоя
|
||||||
|
GBRAIN_REPO="${GBRAIN_REPO:-https://git.zetit.ru/zuevav/gbrain-mirror.git}"
|
||||||
|
|
||||||
|
# RAM в гигабайтах (для тюнинга Postgres)
|
||||||
|
TOTAL_RAM_GB="${TOTAL_RAM_GB:-8}"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Утилиты
|
||||||
|
# ============================================================
|
||||||
|
log() {
|
||||||
|
echo -e "\033[1;34m[$(date +'%H:%M:%S')] $*\033[0m"
|
||||||
|
}
|
||||||
|
|
||||||
|
err() {
|
||||||
|
echo -e "\033[1;31m[ERROR] $*\033[0m" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
require_root() {
|
||||||
|
if [[ $EUID -ne 0 ]]; then
|
||||||
|
err "Этот скрипт должен запускаться от root (sudo)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_ubuntu() {
|
||||||
|
if ! grep -q "Ubuntu 22.04" /etc/os-release 2>/dev/null; then
|
||||||
|
err "Скрипт рассчитан на Ubuntu 22.04 LTS."
|
||||||
|
err "Текущая система:"
|
||||||
|
cat /etc/os-release | grep PRETTY_NAME >&2 || true
|
||||||
|
read -p "Продолжить на свой страх и риск? [y/N] " -n 1 -r
|
||||||
|
echo
|
||||||
|
[[ $REPLY =~ ^[Yy]$ ]] || exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Шаг 1: Базовые пакеты и обновления
|
||||||
|
# ============================================================
|
||||||
|
step_base_packages() {
|
||||||
|
log "Шаг 1: Базовые пакеты"
|
||||||
|
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y -qq \
|
||||||
|
curl wget gnupg lsb-release ca-certificates \
|
||||||
|
build-essential git \
|
||||||
|
ufw fail2ban \
|
||||||
|
htop iotop net-tools \
|
||||||
|
unzip jq \
|
||||||
|
software-properties-common \
|
||||||
|
apt-transport-https \
|
||||||
|
chrony # точное время для аудит-логов
|
||||||
|
|
||||||
|
log "Шаг 1: ✓"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Шаг 2: Создание пользователя zbrain
|
||||||
|
# ============================================================
|
||||||
|
step_create_user() {
|
||||||
|
log "Шаг 2: Пользователь $ZBRAIN_USER"
|
||||||
|
|
||||||
|
if ! id "$ZBRAIN_USER" &>/dev/null; then
|
||||||
|
useradd --system --create-home --shell /bin/bash "$ZBRAIN_USER"
|
||||||
|
log "Создан пользователь $ZBRAIN_USER"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$ZBRAIN_HOME" "$ZBRAIN_CONFIG_DIR" "$ZBRAIN_DATA_DIR" "$ZBRAIN_LOG_DIR"
|
||||||
|
chown -R "$ZBRAIN_USER:$ZBRAIN_USER" "$ZBRAIN_HOME" "$ZBRAIN_DATA_DIR" "$ZBRAIN_LOG_DIR"
|
||||||
|
chmod 750 "$ZBRAIN_CONFIG_DIR" # секреты - 750
|
||||||
|
|
||||||
|
log "Шаг 2: ✓"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Шаг 3: Системные параметры (sysctl, swap, файрвол)
|
||||||
|
# ============================================================
|
||||||
|
step_system_tuning() {
|
||||||
|
log "Шаг 3: Системные параметры"
|
||||||
|
|
||||||
|
# Swap для защиты от OOM
|
||||||
|
if [[ ! -f /swapfile ]]; then
|
||||||
|
fallocate -l 4G /swapfile
|
||||||
|
chmod 600 /swapfile
|
||||||
|
mkswap /swapfile
|
||||||
|
swapon /swapfile
|
||||||
|
echo '/swapfile none swap sw 0 0' >> /etc/fstab
|
||||||
|
log "Создан swap 4G"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# sysctl для Postgres + сети
|
||||||
|
cat > /etc/sysctl.d/99-zbrain.conf <<EOF
|
||||||
|
# ZBrain system tuning
|
||||||
|
vm.swappiness = 10
|
||||||
|
vm.overcommit_memory = 2
|
||||||
|
vm.overcommit_ratio = 80
|
||||||
|
vm.dirty_background_ratio = 5
|
||||||
|
vm.dirty_ratio = 10
|
||||||
|
|
||||||
|
# Сетевые буферы
|
||||||
|
net.core.rmem_max = 16777216
|
||||||
|
net.core.wmem_max = 16777216
|
||||||
|
net.ipv4.tcp_rmem = 4096 87380 16777216
|
||||||
|
net.ipv4.tcp_wmem = 4096 65536 16777216
|
||||||
|
|
||||||
|
# Защита
|
||||||
|
net.ipv4.tcp_syncookies = 1
|
||||||
|
net.ipv4.conf.all.rp_filter = 1
|
||||||
|
EOF
|
||||||
|
sysctl -p /etc/sysctl.d/99-zbrain.conf >/dev/null
|
||||||
|
|
||||||
|
# UFW firewall
|
||||||
|
ufw --force reset >/dev/null
|
||||||
|
ufw default deny incoming
|
||||||
|
ufw default allow outgoing
|
||||||
|
ufw allow 22/tcp comment 'SSH'
|
||||||
|
ufw allow 80/tcp comment 'HTTP (redirect to HTTPS)'
|
||||||
|
ufw allow 443/tcp comment 'HTTPS'
|
||||||
|
# Postgres - ТОЛЬКО localhost, никаких ufw allow 5432!
|
||||||
|
ufw --force enable
|
||||||
|
|
||||||
|
log "Шаг 3: ✓"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Шаг 4: PostgreSQL 16 + pgvector
|
||||||
|
# ============================================================
|
||||||
|
step_postgres() {
|
||||||
|
log "Шаг 4: PostgreSQL $POSTGRES_VERSION + pgvector"
|
||||||
|
|
||||||
|
# Репозиторий PGDG
|
||||||
|
if [[ ! -f /etc/apt/sources.list.d/pgdg.list ]]; then
|
||||||
|
sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
|
||||||
|
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
|
||||||
|
apt-get update -qq
|
||||||
|
fi
|
||||||
|
|
||||||
|
apt-get install -y -qq \
|
||||||
|
postgresql-$POSTGRES_VERSION \
|
||||||
|
postgresql-contrib-$POSTGRES_VERSION \
|
||||||
|
postgresql-$POSTGRES_VERSION-pgvector \
|
||||||
|
postgresql-client-$POSTGRES_VERSION
|
||||||
|
|
||||||
|
# Тюнинг postgresql.conf под объём RAM
|
||||||
|
local pg_conf="/etc/postgresql/$POSTGRES_VERSION/main/postgresql.conf"
|
||||||
|
local shared_buffers=$((TOTAL_RAM_GB * 1024 / 4)) # 25% от RAM в MB
|
||||||
|
local effective_cache=$((TOTAL_RAM_GB * 1024 / 2)) # 50% от RAM в MB
|
||||||
|
|
||||||
|
cat > /etc/postgresql/$POSTGRES_VERSION/main/conf.d/zbrain.conf <<EOF
|
||||||
|
# ZBrain Postgres tuning
|
||||||
|
# Сгенерировано bootstrap-vm.sh для VM ${TOTAL_RAM_GB}GB RAM
|
||||||
|
|
||||||
|
# Память
|
||||||
|
shared_buffers = ${shared_buffers}MB
|
||||||
|
effective_cache_size = ${effective_cache}MB
|
||||||
|
maintenance_work_mem = 512MB
|
||||||
|
work_mem = 32MB
|
||||||
|
|
||||||
|
# Соединения
|
||||||
|
max_connections = 100
|
||||||
|
listen_addresses = 'localhost'
|
||||||
|
|
||||||
|
# WAL
|
||||||
|
wal_buffers = 16MB
|
||||||
|
checkpoint_completion_target = 0.9
|
||||||
|
min_wal_size = 1GB
|
||||||
|
max_wal_size = 4GB
|
||||||
|
|
||||||
|
# Параллелизм (для HNSW index build + сложных запросов)
|
||||||
|
max_parallel_workers_per_gather = 2
|
||||||
|
max_parallel_maintenance_workers = 2
|
||||||
|
|
||||||
|
# SSD
|
||||||
|
random_page_cost = 1.1
|
||||||
|
effective_io_concurrency = 200
|
||||||
|
|
||||||
|
# Логи
|
||||||
|
log_min_duration_statement = 1000
|
||||||
|
log_checkpoints = on
|
||||||
|
log_connections = on
|
||||||
|
log_disconnections = on
|
||||||
|
log_lock_waits = on
|
||||||
|
log_temp_files = 0
|
||||||
|
log_line_prefix = '%t [%p] %u@%d '
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# pg_hba.conf - только localhost
|
||||||
|
cat > /etc/postgresql/$POSTGRES_VERSION/main/pg_hba.conf <<EOF
|
||||||
|
# ZBrain pg_hba.conf
|
||||||
|
local all postgres peer
|
||||||
|
local all all scram-sha-256
|
||||||
|
host all all 127.0.0.1/32 scram-sha-256
|
||||||
|
host all all ::1/128 scram-sha-256
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl enable postgresql
|
||||||
|
systemctl restart postgresql
|
||||||
|
|
||||||
|
# Создание главной БД brainhub и расширений
|
||||||
|
sudo -u postgres psql -c "CREATE DATABASE brainhub;" 2>/dev/null || log "БД brainhub уже существует"
|
||||||
|
sudo -u postgres psql -d brainhub -c "CREATE EXTENSION IF NOT EXISTS vector;"
|
||||||
|
sudo -u postgres psql -d brainhub -c "CREATE EXTENSION IF NOT EXISTS pg_trgm;"
|
||||||
|
sudo -u postgres psql -d brainhub -c "CREATE EXTENSION IF NOT EXISTS pgcrypto;"
|
||||||
|
|
||||||
|
log "Шаг 4: ✓ Postgres работает, BD brainhub готова"
|
||||||
|
log " ВАЖНО: создай пароли для пользователей postgres и brainhub:"
|
||||||
|
log " sudo -u postgres psql"
|
||||||
|
log " ALTER USER postgres PASSWORD '...';"
|
||||||
|
log " CREATE USER brainhub WITH PASSWORD '...';"
|
||||||
|
log " GRANT ALL PRIVILEGES ON DATABASE brainhub TO brainhub;"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Шаг 5: Node.js 22 LTS + Bun
|
||||||
|
# ============================================================
|
||||||
|
step_node_bun() {
|
||||||
|
log "Шаг 5: Node.js $NODE_VERSION LTS + Bun"
|
||||||
|
|
||||||
|
# Node через NodeSource
|
||||||
|
if ! command -v node &>/dev/null || [[ "$(node -v)" != v${NODE_VERSION}* ]]; then
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash -
|
||||||
|
apt-get install -y -qq nodejs
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Bun (для gbrain)
|
||||||
|
if ! sudo -u "$ZBRAIN_USER" bash -c 'command -v bun' &>/dev/null; then
|
||||||
|
sudo -u "$ZBRAIN_USER" bash -c 'curl -fsSL https://bun.sh/install | bash'
|
||||||
|
# Добавим в PATH
|
||||||
|
sudo -u "$ZBRAIN_USER" bash -c 'echo "export PATH=\$HOME/.bun/bin:\$PATH" >> ~/.bashrc'
|
||||||
|
fi
|
||||||
|
|
||||||
|
log " Node: $(node -v)"
|
||||||
|
log " npm: $(npm -v)"
|
||||||
|
log "Шаг 5: ✓"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Шаг 6: Docker + Compose
|
||||||
|
# ============================================================
|
||||||
|
step_docker() {
|
||||||
|
log "Шаг 6: Docker"
|
||||||
|
|
||||||
|
if ! command -v docker &>/dev/null; then
|
||||||
|
# Официальный репо Docker (не snap!)
|
||||||
|
install -m 0755 -d /etc/apt/keyrings
|
||||||
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||||
|
chmod a+r /etc/apt/keyrings/docker.gpg
|
||||||
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||||
|
fi
|
||||||
|
|
||||||
|
# zbrain в группу docker
|
||||||
|
usermod -aG docker "$ZBRAIN_USER"
|
||||||
|
|
||||||
|
systemctl enable docker
|
||||||
|
systemctl start docker
|
||||||
|
|
||||||
|
log " Docker: $(docker --version)"
|
||||||
|
log " Compose: $(docker compose version)"
|
||||||
|
log "Шаг 6: ✓"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Шаг 7: gbrain (клонирование из mirror, установка зависимостей)
|
||||||
|
# ============================================================
|
||||||
|
step_gbrain() {
|
||||||
|
log "Шаг 7: gbrain (из $GBRAIN_REPO)"
|
||||||
|
|
||||||
|
local gbrain_dir="$ZBRAIN_DATA_DIR/gbrain"
|
||||||
|
|
||||||
|
if [[ ! -d "$gbrain_dir" ]]; then
|
||||||
|
sudo -u "$ZBRAIN_USER" git clone "$GBRAIN_REPO" "$gbrain_dir"
|
||||||
|
else
|
||||||
|
log " gbrain уже склонирован в $gbrain_dir"
|
||||||
|
sudo -u "$ZBRAIN_USER" bash -c "cd '$gbrain_dir' && git fetch"
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo -u "$ZBRAIN_USER" bash -c "cd '$gbrain_dir' && git checkout '$GBRAIN_VERSION'"
|
||||||
|
|
||||||
|
# Установка зависимостей через bun (используем FNA-прокси)
|
||||||
|
log " Устанавливаю зависимости gbrain (через FNA proxy)..."
|
||||||
|
sudo -u "$ZBRAIN_USER" bash -c "
|
||||||
|
export HTTPS_PROXY='$FNA_PROXY'
|
||||||
|
export HTTP_PROXY='$FNA_PROXY'
|
||||||
|
export PATH=\$HOME/.bun/bin:\$PATH
|
||||||
|
cd '$gbrain_dir' && bun install
|
||||||
|
"
|
||||||
|
|
||||||
|
log "Шаг 7: ✓"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Шаг 8: Конфиг-файлы и шаблоны
|
||||||
|
# ============================================================
|
||||||
|
step_config_templates() {
|
||||||
|
log "Шаг 8: Шаблоны конфигов"
|
||||||
|
|
||||||
|
# .env шаблон
|
||||||
|
cat > "$ZBRAIN_CONFIG_DIR/env.example" <<'EOF'
|
||||||
|
# ZBrain Environment Configuration
|
||||||
|
# Скопируй в .env и заполни значения
|
||||||
|
|
||||||
|
# === База данных ===
|
||||||
|
DATABASE_URL=postgresql://brainhub:CHANGE_ME@localhost:5432/brainhub
|
||||||
|
POSTGRES_ADMIN_URL=postgresql://postgres:CHANGE_ME@localhost:5432/postgres
|
||||||
|
|
||||||
|
# === FNA Proxy (для исходящих к OpenAI/Anthropic) ===
|
||||||
|
HTTPS_PROXY=http://client_001:PASSWORD@fna.zetit.ru:3128
|
||||||
|
HTTP_PROXY=http://client_001:PASSWORD@fna.zetit.ru:3128
|
||||||
|
NO_PROXY=localhost,127.0.0.1,.zetit.ru,.zetit.local
|
||||||
|
|
||||||
|
# === API ключи ===
|
||||||
|
OPENAI_API_KEY=sk-...
|
||||||
|
ANTHROPIC_API_KEY=sk-ant-...
|
||||||
|
|
||||||
|
# === ZBrain ===
|
||||||
|
ZBRAIN_BASE_URL=https://brain.zetit.ru
|
||||||
|
ZBRAIN_INTERNAL_URL=http://brain.zetit.local
|
||||||
|
SESSION_SECRET=GENERATE_RANDOM_64_CHARS
|
||||||
|
JWT_SECRET=GENERATE_RANDOM_64_CHARS
|
||||||
|
TOKEN_ENCRYPTION_KEY=GENERATE_RANDOM_32_BYTES_HEX
|
||||||
|
|
||||||
|
# === OAuth ===
|
||||||
|
YANDEX_OAUTH_CLIENT_ID=
|
||||||
|
YANDEX_OAUTH_CLIENT_SECRET=
|
||||||
|
GITHUB_OAUTH_CLIENT_ID=
|
||||||
|
GITHUB_OAUTH_CLIENT_SECRET=
|
||||||
|
|
||||||
|
# === Порты ===
|
||||||
|
BRAINHUB_INTERNAL_PORT=3000
|
||||||
|
BRAINHUB_PUBLIC_PORT=3010
|
||||||
|
GBRAIN_PORT_RANGE_START=3001
|
||||||
|
GBRAIN_PORT_RANGE_END=3099
|
||||||
|
|
||||||
|
# === Логирование ===
|
||||||
|
LOG_LEVEL=info
|
||||||
|
LOG_FILE=/var/log/zbrain/brainhub.log
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod 640 "$ZBRAIN_CONFIG_DIR/env.example"
|
||||||
|
chown root:"$ZBRAIN_USER" "$ZBRAIN_CONFIG_DIR/env.example"
|
||||||
|
|
||||||
|
log " Шаблон env: $ZBRAIN_CONFIG_DIR/env.example"
|
||||||
|
log " Не забудь: cp $ZBRAIN_CONFIG_DIR/env.example $ZBRAIN_CONFIG_DIR/.env && chmod 600 $ZBRAIN_CONFIG_DIR/.env"
|
||||||
|
log "Шаг 8: ✓"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Шаг 9: Самопроверка
|
||||||
|
# ============================================================
|
||||||
|
step_verify() {
|
||||||
|
log "Шаг 9: Самопроверка"
|
||||||
|
|
||||||
|
local errors=0
|
||||||
|
|
||||||
|
check() {
|
||||||
|
local name="$1"
|
||||||
|
local cmd="$2"
|
||||||
|
if eval "$cmd" &>/dev/null; then
|
||||||
|
log " ✓ $name"
|
||||||
|
else
|
||||||
|
err " ✗ $name (команда: $cmd)"
|
||||||
|
errors=$((errors+1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check "PostgreSQL запущен" "systemctl is-active postgresql"
|
||||||
|
check "Postgres доступен" "sudo -u postgres psql -c 'SELECT version()'"
|
||||||
|
check "pgvector установлен" "sudo -u postgres psql -d brainhub -c 'SELECT * FROM pg_extension WHERE extname = '\''vector'\''' | grep -q vector"
|
||||||
|
check "Node.js установлен" "node --version"
|
||||||
|
check "Docker запущен" "systemctl is-active docker"
|
||||||
|
check "Bun установлен" "sudo -u $ZBRAIN_USER bash -c 'PATH=\$HOME/.bun/bin:\$PATH bun --version'"
|
||||||
|
check "gbrain склонирован" "test -d $ZBRAIN_DATA_DIR/gbrain"
|
||||||
|
check "UFW активен" "ufw status | grep -q 'Status: active'"
|
||||||
|
check "Swap включён" "swapon --show | grep -q swapfile"
|
||||||
|
|
||||||
|
if [[ $errors -gt 0 ]]; then
|
||||||
|
err "Обнаружено $errors ошибок. Проверь логи выше."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Шаг 9: ✓ Всё работает"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Итоговый отчёт
|
||||||
|
# ============================================================
|
||||||
|
final_report() {
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
╔════════════════════════════════════════════════════════════╗
|
||||||
|
║ ZBrain VM Bootstrap готов ║
|
||||||
|
╚════════════════════════════════════════════════════════════╝
|
||||||
|
|
||||||
|
Следующие шаги:
|
||||||
|
|
||||||
|
1. Создай пароли для Postgres:
|
||||||
|
sudo -u postgres psql
|
||||||
|
ALTER USER postgres PASSWORD 'strong-password';
|
||||||
|
CREATE USER brainhub WITH PASSWORD 'another-strong-password';
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE brainhub TO brainhub;
|
||||||
|
\\q
|
||||||
|
|
||||||
|
2. Скопируй и заполни .env:
|
||||||
|
sudo cp $ZBRAIN_CONFIG_DIR/env.example $ZBRAIN_CONFIG_DIR/.env
|
||||||
|
sudo chmod 600 $ZBRAIN_CONFIG_DIR/.env
|
||||||
|
sudo chown $ZBRAIN_USER:$ZBRAIN_USER $ZBRAIN_CONFIG_DIR/.env
|
||||||
|
sudo nano $ZBRAIN_CONFIG_DIR/.env
|
||||||
|
|
||||||
|
3. Создай первый брейн:
|
||||||
|
sudo bash $ZBRAIN_HOME/scripts/create-brain.sh zetit "ZETIT MSP" 3001
|
||||||
|
|
||||||
|
4. (опционально) Склонируй ZBrain код:
|
||||||
|
sudo -u $ZBRAIN_USER git clone git@git.zetit.ru:zuevav/ZBrain.git $ZBRAIN_HOME/app
|
||||||
|
|
||||||
|
5. Запусти первый gbrain instance:
|
||||||
|
см. docs/DEPLOYMENT.md
|
||||||
|
|
||||||
|
Параметры этой VM:
|
||||||
|
RAM: ${TOTAL_RAM_GB} GB
|
||||||
|
Postgres shared_buffers: $((TOTAL_RAM_GB * 1024 / 4)) MB
|
||||||
|
ZBrain home: $ZBRAIN_HOME
|
||||||
|
ZBrain data: $ZBRAIN_DATA_DIR
|
||||||
|
ZBrain config: $ZBRAIN_CONFIG_DIR
|
||||||
|
ZBrain logs: $ZBRAIN_LOG_DIR
|
||||||
|
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Main
|
||||||
|
# ============================================================
|
||||||
|
main() {
|
||||||
|
require_root
|
||||||
|
check_ubuntu
|
||||||
|
|
||||||
|
log "Старт ZBrain VM bootstrap"
|
||||||
|
log "Целевая RAM: ${TOTAL_RAM_GB} GB"
|
||||||
|
log "FNA proxy: ${FNA_PROXY%%:*}://***"
|
||||||
|
echo
|
||||||
|
|
||||||
|
step_base_packages
|
||||||
|
step_create_user
|
||||||
|
step_system_tuning
|
||||||
|
step_postgres
|
||||||
|
step_node_bun
|
||||||
|
step_docker
|
||||||
|
step_gbrain
|
||||||
|
step_config_templates
|
||||||
|
step_verify
|
||||||
|
|
||||||
|
final_report
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Executable
+273
@@ -0,0 +1,273 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Создание нового gbrain instance
|
||||||
|
# ================================
|
||||||
|
# Создаёт изолированную БД + пользователя Postgres + systemd unit
|
||||||
|
# для gbrain serve --http на отдельном порту.
|
||||||
|
#
|
||||||
|
# Использование:
|
||||||
|
# sudo bash scripts/create-brain.sh <name> "<display name>" <port>
|
||||||
|
#
|
||||||
|
# Примеры:
|
||||||
|
# sudo bash scripts/create-brain.sh zetit "ZETIT MSP" 3001
|
||||||
|
# sudo bash scripts/create-brain.sh telerapharma "TeleraPharma" 3002
|
||||||
|
# sudo bash scripts/create-brain.sh personal "Personal" 3003
|
||||||
|
# sudo bash scripts/create-brain.sh community "Smolenskaya 10" 3004
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Аргументы
|
||||||
|
# ============================================================
|
||||||
|
if [[ $# -lt 3 ]]; then
|
||||||
|
cat <<EOF
|
||||||
|
Usage: $0 <name> "<display-name>" <port>
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
name slug-имя брейна (только [a-z0-9_], <=32 символов)
|
||||||
|
display-name человекочитаемое имя в кавычках
|
||||||
|
port TCP-порт для gbrain serve --http (3001-3099)
|
||||||
|
|
||||||
|
Example:
|
||||||
|
$0 zetit "ZETIT MSP" 3001
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
BRAIN_NAME="$1"
|
||||||
|
BRAIN_DISPLAY="$2"
|
||||||
|
BRAIN_PORT="$3"
|
||||||
|
|
||||||
|
# Валидация
|
||||||
|
if [[ ! "$BRAIN_NAME" =~ ^[a-z0-9_]{1,32}$ ]]; then
|
||||||
|
echo "ERROR: name должно содержать только [a-z0-9_] и быть не длиннее 32 символов" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! "$BRAIN_PORT" =~ ^[0-9]+$ ]] || [[ $BRAIN_PORT -lt 3001 ]] || [[ $BRAIN_PORT -gt 3099 ]]; then
|
||||||
|
echo "ERROR: port должен быть числом в диапазоне 3001-3099" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Константы
|
||||||
|
# ============================================================
|
||||||
|
ZBRAIN_USER="zbrain"
|
||||||
|
ZBRAIN_DATA_DIR="/var/lib/zbrain"
|
||||||
|
ZBRAIN_CONFIG_DIR="/etc/zbrain"
|
||||||
|
GBRAIN_DIR="$ZBRAIN_DATA_DIR/gbrain"
|
||||||
|
|
||||||
|
DB_NAME="gbrain_${BRAIN_NAME}"
|
||||||
|
DB_USER="gbrain_${BRAIN_NAME}"
|
||||||
|
SYSTEMD_UNIT="zbrain-gbrain-${BRAIN_NAME}"
|
||||||
|
BRAIN_HOME="$ZBRAIN_DATA_DIR/brains/$BRAIN_NAME"
|
||||||
|
|
||||||
|
log() { echo -e "\033[1;34m[$(date +'%H:%M:%S')] $*\033[0m"; }
|
||||||
|
err() { echo -e "\033[1;31m[ERROR] $*\033[0m" >&2; }
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Проверки
|
||||||
|
# ============================================================
|
||||||
|
[[ $EUID -eq 0 ]] || { err "Требуется sudo"; exit 1; }
|
||||||
|
[[ -d "$GBRAIN_DIR" ]] || { err "gbrain не установлен. Запусти сначала bootstrap-vm.sh"; exit 1; }
|
||||||
|
[[ -f "$ZBRAIN_CONFIG_DIR/.env" ]] || { err "$ZBRAIN_CONFIG_DIR/.env не существует"; exit 1; }
|
||||||
|
|
||||||
|
# Проверка занятости порта
|
||||||
|
if ss -tln | grep -q ":${BRAIN_PORT} "; then
|
||||||
|
err "Порт $BRAIN_PORT уже занят"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Проверка существования
|
||||||
|
if sudo -u postgres psql -lqt | cut -d \| -f 1 | grep -qw "$DB_NAME"; then
|
||||||
|
err "БД $DB_NAME уже существует"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Создание Postgres пользователя и БД
|
||||||
|
# ============================================================
|
||||||
|
log "Создаю Postgres пользователя $DB_USER и БД $DB_NAME"
|
||||||
|
|
||||||
|
DB_PASSWORD=$(openssl rand -hex 24)
|
||||||
|
|
||||||
|
sudo -u postgres psql <<EOF
|
||||||
|
CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASSWORD}';
|
||||||
|
CREATE DATABASE ${DB_NAME} OWNER ${DB_USER};
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE ${DB_NAME} TO ${DB_USER};
|
||||||
|
EOF
|
||||||
|
|
||||||
|
sudo -u postgres psql -d "$DB_NAME" <<EOF
|
||||||
|
CREATE EXTENSION IF NOT EXISTS vector;
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||||
|
GRANT ALL ON SCHEMA public TO ${DB_USER};
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log "✓ БД создана"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Brain home directory + конфиг
|
||||||
|
# ============================================================
|
||||||
|
log "Создаю $BRAIN_HOME"
|
||||||
|
mkdir -p "$BRAIN_HOME"
|
||||||
|
chown -R "$ZBRAIN_USER:$ZBRAIN_USER" "$BRAIN_HOME"
|
||||||
|
|
||||||
|
# gbrain config
|
||||||
|
cat > "$BRAIN_HOME/config.json" <<EOF
|
||||||
|
{
|
||||||
|
"name": "${BRAIN_NAME}",
|
||||||
|
"display_name": "${BRAIN_DISPLAY}",
|
||||||
|
"database_url": "postgresql://${DB_USER}:${DB_PASSWORD}@localhost:5432/${DB_NAME}",
|
||||||
|
"port": ${BRAIN_PORT},
|
||||||
|
"data_dir": "${BRAIN_HOME}/data"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
chown "$ZBRAIN_USER:$ZBRAIN_USER" "$BRAIN_HOME/config.json"
|
||||||
|
chmod 600 "$BRAIN_HOME/config.json"
|
||||||
|
|
||||||
|
mkdir -p "$BRAIN_HOME/data"
|
||||||
|
chown -R "$ZBRAIN_USER:$ZBRAIN_USER" "$BRAIN_HOME/data"
|
||||||
|
|
||||||
|
log "✓ Конфиг $BRAIN_HOME/config.json"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Инициализация gbrain схемы
|
||||||
|
# ============================================================
|
||||||
|
log "Инициализирую gbrain схему"
|
||||||
|
|
||||||
|
# Загружаем .env для FNA proxy + OpenAI/Anthropic ключей
|
||||||
|
set -a
|
||||||
|
source "$ZBRAIN_CONFIG_DIR/.env"
|
||||||
|
set +a
|
||||||
|
|
||||||
|
sudo -u "$ZBRAIN_USER" bash -c "
|
||||||
|
export PATH=\$HOME/.bun/bin:\$PATH
|
||||||
|
export HTTPS_PROXY='${HTTPS_PROXY}'
|
||||||
|
export HTTP_PROXY='${HTTP_PROXY}'
|
||||||
|
export OPENAI_API_KEY='${OPENAI_API_KEY}'
|
||||||
|
export ANTHROPIC_API_KEY='${ANTHROPIC_API_KEY}'
|
||||||
|
cd '$GBRAIN_DIR' && \
|
||||||
|
bun run gbrain init --url 'postgresql://${DB_USER}:${DB_PASSWORD}@localhost:5432/${DB_NAME}' --yes
|
||||||
|
"
|
||||||
|
|
||||||
|
log "✓ Схема инициализирована"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Systemd unit
|
||||||
|
# ============================================================
|
||||||
|
log "Создаю systemd unit $SYSTEMD_UNIT"
|
||||||
|
|
||||||
|
cat > "/etc/systemd/system/${SYSTEMD_UNIT}.service" <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=ZBrain gbrain instance: ${BRAIN_DISPLAY}
|
||||||
|
Documentation=https://git.zetit.ru/zuevav/ZBrain
|
||||||
|
After=postgresql.service network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
Requires=postgresql.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=${ZBRAIN_USER}
|
||||||
|
Group=${ZBRAIN_USER}
|
||||||
|
WorkingDirectory=${GBRAIN_DIR}
|
||||||
|
|
||||||
|
# Окружение
|
||||||
|
Environment="PATH=/home/${ZBRAIN_USER}/.bun/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||||
|
Environment="DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@localhost:5432/${DB_NAME}"
|
||||||
|
Environment="GBRAIN_DATA_DIR=${BRAIN_HOME}/data"
|
||||||
|
EnvironmentFile=${ZBRAIN_CONFIG_DIR}/.env
|
||||||
|
|
||||||
|
# Запуск gbrain в HTTP MCP режиме
|
||||||
|
ExecStart=/home/${ZBRAIN_USER}/.bun/bin/bun run gbrain serve --http --port ${BRAIN_PORT} --bind 127.0.0.1
|
||||||
|
|
||||||
|
# Restart policy
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10s
|
||||||
|
StartLimitInterval=300
|
||||||
|
StartLimitBurst=5
|
||||||
|
|
||||||
|
# Security hardening
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
ReadWritePaths=${BRAIN_HOME} /var/log/zbrain
|
||||||
|
ProtectKernelTunables=true
|
||||||
|
ProtectKernelModules=true
|
||||||
|
ProtectControlGroups=true
|
||||||
|
RestrictNamespaces=true
|
||||||
|
RestrictRealtime=true
|
||||||
|
LockPersonality=true
|
||||||
|
|
||||||
|
# Resource limits
|
||||||
|
LimitNOFILE=65536
|
||||||
|
MemoryMax=2G
|
||||||
|
CPUQuota=200%
|
||||||
|
|
||||||
|
# Логи
|
||||||
|
StandardOutput=append:/var/log/zbrain/${BRAIN_NAME}.log
|
||||||
|
StandardError=append:/var/log/zbrain/${BRAIN_NAME}.error.log
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable "$SYSTEMD_UNIT"
|
||||||
|
systemctl start "$SYSTEMD_UNIT"
|
||||||
|
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
if systemctl is-active --quiet "$SYSTEMD_UNIT"; then
|
||||||
|
log "✓ Сервис запущен: $SYSTEMD_UNIT"
|
||||||
|
else
|
||||||
|
err "Сервис не стартовал. Логи:"
|
||||||
|
journalctl -u "$SYSTEMD_UNIT" -n 20 --no-pager
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Проверка health
|
||||||
|
# ============================================================
|
||||||
|
log "Проверяю доступность на localhost:$BRAIN_PORT"
|
||||||
|
|
||||||
|
if curl -sf "http://localhost:${BRAIN_PORT}/health" >/dev/null 2>&1; then
|
||||||
|
log "✓ Health check OK"
|
||||||
|
else
|
||||||
|
log "⚠ Health endpoint недоступен (возможно gbrain ещё стартует или endpoint называется иначе)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Финальный отчёт
|
||||||
|
# ============================================================
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
╔════════════════════════════════════════════════════════════╗
|
||||||
|
║ Брейн '${BRAIN_NAME}' создан
|
||||||
|
╚════════════════════════════════════════════════════════════╝
|
||||||
|
|
||||||
|
Display name: ${BRAIN_DISPLAY}
|
||||||
|
Postgres DB: ${DB_NAME}
|
||||||
|
Postgres user: ${DB_USER}
|
||||||
|
Port: ${BRAIN_PORT}
|
||||||
|
Home: ${BRAIN_HOME}
|
||||||
|
Systemd unit: ${SYSTEMD_UNIT}
|
||||||
|
MCP endpoint: http://localhost:${BRAIN_PORT}/mcp
|
||||||
|
|
||||||
|
Полезные команды:
|
||||||
|
systemctl status ${SYSTEMD_UNIT}
|
||||||
|
journalctl -u ${SYSTEMD_UNIT} -f
|
||||||
|
tail -f /var/log/zbrain/${BRAIN_NAME}.log
|
||||||
|
|
||||||
|
Импорт данных:
|
||||||
|
sudo -u ${ZBRAIN_USER} bash -c '
|
||||||
|
export PATH=\$HOME/.bun/bin:\$PATH
|
||||||
|
export DATABASE_URL="postgresql://${DB_USER}:***@localhost:5432/${DB_NAME}"
|
||||||
|
cd ${GBRAIN_DIR} && bun run gbrain import /path/to/markdown/
|
||||||
|
'
|
||||||
|
|
||||||
|
DB password сохранён в:
|
||||||
|
${BRAIN_HOME}/config.json (доступ только root и ${ZBRAIN_USER})
|
||||||
|
|
||||||
|
EOF
|
||||||
Executable
+153
@@ -0,0 +1,153 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# ZBrain Restore
|
||||||
|
# ===============
|
||||||
|
# Восстанавливает ZBrain из бэкапа созданного backup.sh.
|
||||||
|
#
|
||||||
|
# Использование:
|
||||||
|
# sudo bash scripts/restore.sh <backup-timestamp>
|
||||||
|
#
|
||||||
|
# Где <backup-timestamp> - имя папки в BACKUP_DIR (например, 20260520-031500)
|
||||||
|
#
|
||||||
|
# ВАЖНО: для расшифровки нужен приватный age ключ.
|
||||||
|
# Положи его на VM перед запуском restore (например, /tmp/backup.age).
|
||||||
|
# Скрипт спросит путь к ключу.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Конфигурация
|
||||||
|
# ============================================================
|
||||||
|
BACKUP_DIR="${BACKUP_DIR:-/var/backups/zbrain}"
|
||||||
|
LOG_FILE="/var/log/zbrain/restore.log"
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
err() {
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $*" | tee -a "$LOG_FILE" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Аргументы
|
||||||
|
# ============================================================
|
||||||
|
if [[ $# -lt 1 ]]; then
|
||||||
|
echo "Usage: $0 <backup-timestamp>"
|
||||||
|
echo ""
|
||||||
|
echo "Available backups:"
|
||||||
|
ls -1 "$BACKUP_DIR" 2>/dev/null | grep -E '^[0-9]{8}-[0-9]{6}$' | sort -r | head -10
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TIMESTAMP="$1"
|
||||||
|
BACKUP_PATH="$BACKUP_DIR/$TIMESTAMP"
|
||||||
|
|
||||||
|
[[ -d "$BACKUP_PATH" ]] || { err "Бэкап не найден: $BACKUP_PATH"; exit 1; }
|
||||||
|
[[ $EUID -eq 0 ]] || { err "Требуется sudo"; exit 1; }
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Age ключ для расшифровки
|
||||||
|
# ============================================================
|
||||||
|
read -p "Путь к age приватному ключу: " AGE_KEY_PATH
|
||||||
|
[[ -f "$AGE_KEY_PATH" ]] || { err "Файл не найден: $AGE_KEY_PATH"; exit 1; }
|
||||||
|
|
||||||
|
# Тест ключа
|
||||||
|
if ! age -d -i "$AGE_KEY_PATH" "$BACKUP_PATH/_globals.sql.age" > /dev/null 2>&1; then
|
||||||
|
err "Не могу расшифровать с этим ключом"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Подтверждение
|
||||||
|
# ============================================================
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
╔════════════════════════════════════════════════════════════╗
|
||||||
|
║ ВНИМАНИЕ ║
|
||||||
|
║ ║
|
||||||
|
║ Восстановление перезапишет все существующие БД ZBrain! ║
|
||||||
|
║ ║
|
||||||
|
║ Бэкап: $TIMESTAMP ║
|
||||||
|
║ Файлы: ║
|
||||||
|
$(ls "$BACKUP_PATH" | sed 's/^/║ /' | awk '{printf "%-60s║\n", $0}')
|
||||||
|
║ ║
|
||||||
|
╚════════════════════════════════════════════════════════════╝
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
read -p "Введите 'YES' для продолжения: " CONFIRM
|
||||||
|
[[ "$CONFIRM" == "YES" ]] || { log "Отменено пользователем"; exit 0; }
|
||||||
|
|
||||||
|
log "=== Начинаю restore из $TIMESTAMP ==="
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Восстановление глобальных объектов
|
||||||
|
# ============================================================
|
||||||
|
log "Восстанавливаю глобальные Postgres объекты..."
|
||||||
|
age -d -i "$AGE_KEY_PATH" -o /tmp/_globals.sql "$BACKUP_PATH/_globals.sql.age"
|
||||||
|
sudo -u postgres psql -f /tmp/_globals.sql 2>&1 | tee -a "$LOG_FILE" | grep -v "already exists" || true
|
||||||
|
rm /tmp/_globals.sql
|
||||||
|
log "✓ Globals"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Восстановление БД
|
||||||
|
# ============================================================
|
||||||
|
for encrypted_dump in "$BACKUP_PATH"/*.sql.dump.age; do
|
||||||
|
[[ -f "$encrypted_dump" ]] || continue
|
||||||
|
db_name=$(basename "$encrypted_dump" .sql.dump.age)
|
||||||
|
[[ "$db_name" == "_globals" ]] && continue
|
||||||
|
|
||||||
|
log "Восстанавливаю $db_name..."
|
||||||
|
|
||||||
|
# Дропаем существующую БД (если есть)
|
||||||
|
sudo -u postgres dropdb --if-exists "$db_name"
|
||||||
|
|
||||||
|
# Создаём пустую
|
||||||
|
sudo -u postgres createdb "$db_name"
|
||||||
|
|
||||||
|
# Расшифровываем dump
|
||||||
|
dump_file="/tmp/${db_name}.dump"
|
||||||
|
age -d -i "$AGE_KEY_PATH" -o "$dump_file" "$encrypted_dump"
|
||||||
|
|
||||||
|
# Восстанавливаем
|
||||||
|
sudo -u postgres pg_restore --dbname="$db_name" --no-owner --role=postgres "$dump_file" 2>&1 | tee -a "$LOG_FILE" || true
|
||||||
|
|
||||||
|
rm "$dump_file"
|
||||||
|
log "✓ $db_name"
|
||||||
|
done
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Конфиги (опционально)
|
||||||
|
# ============================================================
|
||||||
|
read -p "Восстановить конфиги? (включая /etc/zbrain) [y/N]: " RESTORE_CONFIG
|
||||||
|
if [[ "$RESTORE_CONFIG" =~ ^[Yy]$ ]]; then
|
||||||
|
log "Восстанавливаю конфиги..."
|
||||||
|
age -d -i "$AGE_KEY_PATH" -o /tmp/config.tar.gz "$BACKUP_PATH/config.tar.gz.age"
|
||||||
|
tar xzf /tmp/config.tar.gz -C /
|
||||||
|
rm /tmp/config.tar.gz
|
||||||
|
log "✓ Конфиги"
|
||||||
|
|
||||||
|
if [[ -f "$BACKUP_PATH/brains-meta.tar.gz.age" ]]; then
|
||||||
|
log "Восстанавливаю метаданные брейнов..."
|
||||||
|
age -d -i "$AGE_KEY_PATH" -o /tmp/brains-meta.tar.gz "$BACKUP_PATH/brains-meta.tar.gz.age"
|
||||||
|
tar xzf /tmp/brains-meta.tar.gz -C /var/lib/zbrain/
|
||||||
|
rm /tmp/brains-meta.tar.gz
|
||||||
|
log "✓ Brain configs"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Финал
|
||||||
|
# ============================================================
|
||||||
|
log "=== Restore завершён ==="
|
||||||
|
log ""
|
||||||
|
log "Следующие шаги:"
|
||||||
|
log " 1. Перезапусти системные сервисы:"
|
||||||
|
log " sudo systemctl restart 'zbrain-gbrain-*.service'"
|
||||||
|
log " 2. Перезапусти brainhub:"
|
||||||
|
log " cd /opt/zbrain/deploy/docker && docker compose restart"
|
||||||
|
log " 3. Проверь health endpoint каждого сервиса"
|
||||||
|
log ""
|
||||||
|
log "ВАЖНО: удали age приватный ключ с этой VM после restore!"
|
||||||
|
log " shred -u $AGE_KEY_PATH"
|
||||||
Reference in New Issue
Block a user