This commit is contained in:
zuevav
2026-05-20 19:33:02 +03:00
commit f4bca8449e
30 changed files with 4152 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
# ADR-0001: Vendored gbrain через git mirror
**Дата:** 2026-05-20
**Статус:** Принято
## Контекст
gbrain (https://github.com/garrytan/gbrain) — активно развивающийся проект:
- 60+ коммитов за последние 2 месяца
- v0.26 (текущая) ввела breaking change: переход с bearer tokens на OAuth 2.1
- Public API субпакетов (`gbrain/operations`, `gbrain/pglite-engine`) меняется
Дополнительные факторы:
- GitHub аккаунт zuevav был ограничен в апреле 2026, доступ к публичным репозиториям через `bun install -g github:garrytan/gbrain` может неожиданно ломаться
- ZBrain зависит от стабильности gbrain в production
- При следующей блокировке аккаунта мы не сможем переустановить или обновить gbrain
## Решение
Не зависеть от GitHub в runtime/install path. Зеркалить gbrain в свой git:
```bash
# Однократно, на чистом ноуте с работающим GitHub доступом:
git clone --mirror https://github.com/garrytan/gbrain.git
cd gbrain.git
git remote set-url --push origin git@git.zetit.ru:zuevav/gbrain-mirror.git
git push --mirror
```
Bootstrap-скрипт клонит из git.zetit.ru:
```bash
GBRAIN_REPO="https://git.zetit.ru/zuevav/gbrain-mirror.git" \
GBRAIN_VERSION="v0.26.5" \
bash scripts/bootstrap-vm.sh
```
## Последствия
### Плюсы
- Независимость от GitHub доступности и блокировок аккаунтов
- Управляемое обновление gbrain (через `git fetch upstream && review && push to mirror`)
- Возможность hotfix'ов локально, если upstream сломан (как форк, но в общем git)
- Защита от supply chain атак (мы видим diff перед обновлением)
### Минусы
- Нужно вручную обновлять mirror (раз в месяц-два)
- Лишний шаг в setup
- Расхождение с upstream если хотфикс не запушим обратно
### Митигация
- Раз в месяц: проверка upstream'а, review diff, push to mirror
- Все наши изменения (если потребуются) — в отдельной ветке `zetit/*`, чтобы не мерджились с upstream
- Tag'ируем версии в нашем mirror'е (`v0.26.5-zetit-1`, если есть наши патчи)
## Альтернативы рассмотренные
1. **Прямая зависимость от github:garrytan/gbrain** — отвергнуто из-за рисков блокировки и breaking changes
2. **Свой fork на git.zetit.ru с активной разработкой** — overkill, мы не планируем существенно дорабатывать gbrain
3. **npm-publish gbrain в свой private npm registry** — gbrain не публикуется в npm как пакет; нужен install via bun + workspace
@@ -0,0 +1,92 @@
# ADR-0002: Two-zone deployment (внутренний + публичный контур)
**Дата:** 2026-05-20
**Статус:** Принято
## Контекст
ZBrain должен обслуживать:
1. **Внутренних пользователей** — owner (Алексей) и будущая команда работают через VPN/локальную сеть ZETIT. Им нужен полный UI: дашборд, управление брейнами, токенами, источниками, аудит.
2. **Внешних агентов** — Claude Code на серверах клиентов (TeleraPharma, Fontvielle, и т.д.), которым нужен MCP-доступ к чтению/записи брейнов. У этих серверов нет VPN-доступа к ZETIT.
Простое решение "выставить всё на публичный домен с auth" имеет недостатки:
- Любая уязвимость в админ-UI становится публично доступной
- OAuth flow на админ-эндпоинтах усложняет admin интеграции (curl, скрипты)
- Утечка session cookie с админ-привилегиями через CSRF/XSS критична
## Решение
Развести функционал по двум сетевым контурам с разными доменами:
### Внутренний контур (brain.zetit.local)
- Доступен **только** из сети ZETIT и через VPN
- Plain HTTP допустим (доверенная сеть, нет MITM риска)
- Endpoints: **всё**`/`, `/api/*`, `/mcp/*`, `/api/events` (SSE)
- Auth: session cookie через local login (email/password) или OAuth
- Используется: для web-UI, админских скриптов, debug'а
### Публичный контур (brain.zetit.ru)
- Доступен из интернета через TLS
- Endpoints: **только** `/mcp/*`, `/oauth/*`, `/health`
- Auth: Bearer tokens (для MCP) или OAuth flow (для подключения нового клиента)
- Любой другой path → 404 (без подсказок что вообще существует)
- Rate limiting per IP и per token
- Используется: для удалённого Claude Code/Cursor, для машинного доступа
## Реализация
Один Node.js процесс запускает два независимых Express app на разных портах:
```typescript
// apps/api/src/main.ts
const internalApp = createInternalApp(); // полный набор routes
const publicApp = createPublicApp(); // только /mcp и /oauth
internalApp.listen(3000, '127.0.0.1');
publicApp.listen(3010, '127.0.0.1');
```
Nginx разруливает по hostname:
```nginx
server {
server_name brain.zetit.local;
location / { proxy_pass http://127.0.0.1:3000; }
}
server {
server_name brain.zetit.ru;
listen 443 ssl;
location /mcp/ { proxy_pass http://127.0.0.1:3010; }
location /oauth/ { proxy_pass http://127.0.0.1:3010; }
location / { return 404; }
}
```
## Последствия
### Плюсы
- Атака на админ-UI требует VPN или физического доступа к сети
- Невозможно случайно выставить админ-API в публичный контур (architectural enforcement)
- Rate limit на публичном контуре более агрессивный, не мешает админам
- Раздельные access логи nginx для security audit
### Минусы
- Сложнее тестировать: нужно эмулировать два контура локально
- DNS: две A-записи на разных серверах (внутренний и edge)
- Невозможно "быстро дать админский доступ" партнёру не давая VPN
### Митигация для разработки
- Локально оба app слушают `127.0.0.1` на разных портах, host header проверяется в middleware
- `.env.development` имеет флаг `DISABLE_ZONE_CHECK=true` для упрощённого dev режима
- Integration тесты гоняют оба контура
## Альтернативы рассмотренные
1. **Один контур с строгой ролевой моделью** — отвергнуто, ошибка в RBAC становится критичной уязвимостью
2. **Два разных процесса/контейнера** — overkill для одной VM, осложняет shared state (token cache)
3. **API Gateway (Kong/Traefik)** — лишняя сущность для проекта такого размера
+103
View File
@@ -0,0 +1,103 @@
# ADR-0003: Изоляция брейнов через отдельные PostgreSQL БД
**Дата:** 2026-05-20
**Статус:** Принято
## Контекст
ZBrain хостит несколько брейнов с разным уровнем чувствительности данных:
- **zetit** — внутренние процессы ZETIT, не публичные
- **telerapharma** — корпоративная инфраструктура с NDA
- **personal** — личные данные владельца
- **community** — данные о Смоленской 10, дом-чат, ЖКХ
Утечка MCP токена с правами на один брейн **не должна** давать доступ к другим брейнам.
Варианты изоляции в Postgres (от слабой к сильной):
1. **Одна БД, одна схема, отдельные таблицы с префиксами** — слабая изоляция, любой SQL injection даёт доступ ко всему
2. **Одна БД, отдельные schema'ы (zetit.pages, telerapharma.pages)** — изоляция через `search_path`, но один Postgres user видит всё
3. **Одна БД, отдельные пользователи с row-level security** — сложно, легко допустить ошибку в RLS политиках
4. **Отдельные БД с отдельными пользователями** — изоляция на уровне СУБД
## Решение
Вариант 4: **отдельная БД + отдельный Postgres-пользователь на каждый брейн**.
```
PostgreSQL cluster
├── brainhub (БД)
│ └── владелец: brainhub
│ └── таблицы: users, sessions, tokens, brains, sources, projects, audit_log
├── gbrain_zetit (БД)
│ └── владелец: gbrain_zetit
│ └── полная схема gbrain (pages, chunks, embeddings, links, ...)
├── gbrain_telerapharma (БД)
│ └── владелец: gbrain_telerapharma
│ └── ...
├── gbrain_personal (БД)
│ └── владелец: gbrain_personal
│ └── ...
└── gbrain_community (БД)
└── владелец: gbrain_community
└── ...
```
Каждый gbrain-instance подключается к Postgres под своим пользователем и **физически не видит** другие БД:
```sql
-- Под пользователем gbrain_zetit
\l
-- Покажет только gbrain_zetit (и template/postgres)
\c gbrain_telerapharma
-- ERROR: permission denied for database gbrain_telerapharma
```
Brainhub использует свою БД `brainhub` под пользователем `brainhub` (тоже не видит остальные).
## Реализация
В `scripts/create-brain.sh`:
```sql
CREATE USER gbrain_${BRAIN_NAME} WITH PASSWORD '${random_password}';
CREATE DATABASE gbrain_${BRAIN_NAME} OWNER gbrain_${BRAIN_NAME};
GRANT ALL PRIVILEGES ON DATABASE gbrain_${BRAIN_NAME} TO gbrain_${BRAIN_NAME};
-- НЕ выдаём этому юзеру ничего на other databases
```
Пароль для каждого gbrain-юзера генерируется случайно и хранится:
1. В `/var/lib/zbrain/brains/<name>/config.json` (chmod 600, owner zbrain)
2. В DATABASE_URL внутри systemd unit (Environment="DATABASE_URL=...")
`brainhub` НЕ хранит эти пароли в своей БД (они не нужны для admin-операций) — только знает на каком порту слушает gbrain instance.
## Последствия
### Плюсы
- Физическая изоляция через ACL Postgres
- SQL injection в одном брейне не даёт доступ к другим
- pg_dump каждого брейна тривиально по отдельности
- Удаление брейна = одна команда `DROP DATABASE gbrain_X`
- Разные параметры тюнинга на разные брейны (если потребуется)
### Минусы
- Нельзя сделать JOIN между брейнами в SQL (но это и не нужно — поиск через brainhub)
- Каждая БД имеет свои индексы, общая нагрузка на shared_buffers Postgres делится
- Backup делает дамп каждой БД отдельно (несложно)
### Митигация
- shared_buffers задан с запасом (2GB на 8GB RAM)
- HNSW индексы строятся в `maintenance_work_mem=512MB`, одного брейна за раз достаточно
## Альтернативы рассмотренные
1. **Row-level security в одной БД** — сложно, требует консистентного применения политик на каждой таблице gbrain (а у gbrain их десятки). Один пропущенный CREATE POLICY = утечка
2. **Отдельные Postgres-кластеры на каждый брейн** — overkill, 4× процессов, 4× shared_buffers
3. **Отдельные VM на каждый брейн** — экстремальная изоляция, не нужна на нашем масштабе