commit f4bca8449ef3d64835708893ce700b76c48749e2 Author: zuevav <34027267+zuevav@users.noreply.github.com> Date: Wed May 20 19:33:02 2026 +0300 main diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..34342b4 --- /dev/null +++ b/CLAUDE.md @@ -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 diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..061c8fc --- /dev/null +++ b/INSTALL.md @@ -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@ +``` + +В `~/.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-` и `/var/log/zbrain/.log` +- brainhub (когда будет): `/var/log/zbrain/brainhub.log` +- nginx: `/var/log/nginx/zbrain-*.log` diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4f507e0 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6aa9130 --- /dev/null +++ b/README.md @@ -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:`, `mcp:write:`, `mcp:admin:`) +- Все 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). diff --git a/apps/api/package.json b/apps/api/package.json new file mode 100644 index 0000000..b93f48b --- /dev/null +++ b/apps/api/package.json @@ -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" + } +} diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json new file mode 100644 index 0000000..dc80150 --- /dev/null +++ b/apps/api/tsconfig.json @@ -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"] +} diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..1095b6c --- /dev/null +++ b/apps/web/package.json @@ -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" + } +} diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json new file mode 100644 index 0000000..8e4cf9e --- /dev/null +++ b/apps/web/tsconfig.json @@ -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"] +} diff --git a/deploy/docker/docker-compose.dev.yml b/deploy/docker/docker-compose.dev.yml new file mode 100644 index 0000000..a56c9c7 --- /dev/null +++ b/deploy/docker/docker-compose.dev.yml @@ -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 diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml new file mode 100644 index 0000000..14b5452 --- /dev/null +++ b/deploy/docker/docker-compose.yml @@ -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. diff --git a/deploy/docker/init-dev.sql b/deploy/docker/init-dev.sql new file mode 100644 index 0000000..ac9e797 --- /dev/null +++ b/deploy/docker/init-dev.sql @@ -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 расширения уже созданы выше, миграции прокатятся через приложение diff --git a/deploy/nginx/zbrain.conf b/deploy/nginx/zbrain.conf new file mode 100644 index 0000000..90955aa --- /dev/null +++ b/deploy/nginx/zbrain.conf @@ -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; + } +} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..64dd58f --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -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/, остальные работают | +| 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 месяцев на реальных метриках. diff --git a/docs/DECISIONS/0001-vendored-gbrain.md b/docs/DECISIONS/0001-vendored-gbrain.md new file mode 100644 index 0000000..8f7e9c6 --- /dev/null +++ b/docs/DECISIONS/0001-vendored-gbrain.md @@ -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 diff --git a/docs/DECISIONS/0002-two-zone-deployment.md b/docs/DECISIONS/0002-two-zone-deployment.md new file mode 100644 index 0000000..58b9d82 --- /dev/null +++ b/docs/DECISIONS/0002-two-zone-deployment.md @@ -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)** — лишняя сущность для проекта такого размера diff --git a/docs/DECISIONS/0003-postgres-isolation.md b/docs/DECISIONS/0003-postgres-isolation.md new file mode 100644 index 0000000..5d8ca11 --- /dev/null +++ b/docs/DECISIONS/0003-postgres-isolation.md @@ -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//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 на каждый брейн** — экстремальная изоляция, не нужна на нашем масштабе diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 0000000..4957538 --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -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@` +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 +``` + +На внешнем DNS (если используешь публичный контур): + +``` +brain.zetit.ru. A +``` + +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_" + } + } + } +} +``` + +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% места. diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md new file mode 100644 index 0000000..3397dfc --- /dev/null +++ b/docs/OPERATIONS.md @@ -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 "" + +# Пример +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 = '', + 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 " \ + -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/, в UI брейн помечен красным. + +```bash +# 1. Проверить systemd unit +sudo systemctl status zbrain-gbrain- + +# 2. Если down - последние логи +sudo journalctl -u zbrain-gbrain- -n 100 --no-pager + +# 3. Если crash loop - проверить Postgres connection +sudo -u postgres psql -d gbrain_ -c 'SELECT version()' + +# 4. Если ОК - перезапустить +sudo systemctl restart zbrain-gbrain- + +# 5. Если не помогло - временно отключить и эскалировать +sudo systemctl stop zbrain-gbrain- +``` + +### Сценарий: 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= +# 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='' +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 при необходимости diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 0000000..547c22d --- /dev/null +++ b/docs/ROADMAP.md @@ -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 diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 0000000..832562a --- /dev/null +++ b/docs/SECURITY.md @@ -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_ +``` + +Префикс `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:: +``` + +Уровни: +- `mcp:read:` — query, search, get_page, get_tags, get_links, get_backlinks, get_timeline, get_stats, get_health, get_versions +- `mcp:write:` — всё из read + put_page, add_tag, remove_tag, add_link, remove_link, add_timeline_entry, sync_brain +- `mcp:admin:` — всё из 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 { + // 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: +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//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 diff --git a/package.json b/package.json new file mode 100644 index 0000000..51a5340 --- /dev/null +++ b/package.json @@ -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": "Зуев Алексей Викторович ", + "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" +} diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 0000000..12673ae --- /dev/null +++ b/packages/shared/package.json @@ -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" + } +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts new file mode 100644 index 0000000..e903b98 --- /dev/null +++ b/packages/shared/src/index.ts @@ -0,0 +1,2 @@ +export * from './types.js'; +export * from './schemas.js'; diff --git a/packages/shared/src/schemas.ts b/packages/shared/src/schemas.ts new file mode 100644 index 0000000..1cb2df7 --- /dev/null +++ b/packages/shared/src/schemas.ts @@ -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::' +) 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(), +}); diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts new file mode 100644 index 0000000..7f83742 --- /dev/null +++ b/packages/shared/src/types.ts @@ -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 | null; + ip: string | null; + userAgent: string | null; + createdAt: string; +} + +// API responses + +export interface PaginatedResponse { + items: T[]; + total: number; + page: number; + pageSize: number; +} + +export interface ApiError { + error: string; + code: string; + details?: Record; +} diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 0000000..7c760d8 --- /dev/null +++ b/packages/shared/tsconfig.json @@ -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"] +} diff --git a/scripts/backup.sh b/scripts/backup.sh new file mode 100755 index 0000000..d064a41 --- /dev/null +++ b/scripts/backup.sh @@ -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" <&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 </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 < /etc/postgresql/$POSTGRES_VERSION/main/pg_hba.conf </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 < "" +# +# Примеры: +# 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 < "" + +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 < "$BRAIN_HOME/config.json" < "/etc/systemd/system/${SYSTEMD_UNIT}.service" </dev/null 2>&1; then + log "✓ Health check OK" +else + log "⚠ Health endpoint недоступен (возможно gbrain ещё стартует или endpoint называется иначе)" +fi + +# ============================================================ +# Финальный отчёт +# ============================================================ +cat < +# +# Где - имя папки в 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 " + 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 <&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"