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