CI / ruff · mypy · pytest (pull_request) Failing after 3m40s
- app/pipeline: протокол CIProvider, клиент Gitea Actions (прогон по head_sha, упавший шаг, лог джоба, ретраи), PipelineWatcher — опрос только при незавершённых push, сообщение о push обновляется CI ⏳ → ✅/❌ → CD 🚀/⛔, автору — отдельные уведомления - деплой сервисов — только после зелёного CI; старый push не откатывает сервис, который развернёт более новый; после рестарта бот продолжает с того же места - CI_PROVIDER=none — прежнее поведение (деплой сразу после push) - шаблон tgit-uploads: .gitea/workflows/check.yml; .gitea/ защищён от правок из бота - миграция 0002: старые push помечены skipped, чтобы не слать им «таймаут CI» - ADR-016, ARCHITECTURE §2.5/§5.2, README, DEPLOY, .env.example Refs #11 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
311 lines
30 KiB
Markdown
311 lines
30 KiB
Markdown
# Архитектура tgit
|
||
|
||
> Требования — в [`SPEC.md`](SPEC.md) (v2), решения и их обоснования — в [`DECISIONS.md`](DECISIONS.md).
|
||
|
||
## 1. Общая схема
|
||
|
||
```
|
||
Telegram ◄──long polling──┐
|
||
│
|
||
┌────────────┴──────────────────────────────── хост (VPS / ноутбук) ───────────┐
|
||
│ tgit-bot (один процесс, asyncio) │
|
||
│ ├─ bot/ меню, FSM, роутеры разделов, middleware доступа │
|
||
│ ├─ access/ приглашения (QR), участники, антиперебор │
|
||
│ ├─ repo/ дерево репозитория, черновики, Publisher (git + SSH) ────────┼──► tgit-uploads (Gitea)
|
||
│ ├─ pipeline/ PipelineWatcher: CI по sha → CD, уведомления автору ◄────────┼─── Gitea Actions API
|
||
│ ├─ runtime/ состояние контейнеров, проверки, логи │
|
||
│ ├─ logs/ grep-движок (Python, без shell) │
|
||
│ ├─ agent/ фильтр → LLM-страж → LLM → LogQuery ─────────────────────────┼──► LLM API
|
||
│ └─ alerts/ детекторы, антиспам, рассылка всем, восстановление │
|
||
│ │ │ │
|
||
│ ┌────┴─────┐ ┌────────┴────────┐ │
|
||
│ │ postgres │ │ docker-proxy │ (только GET: containers/logs/ │
|
||
│ └──────────┘ │ │ events/stats) │
|
||
│ └────────┬────────┘ │
|
||
│ │ /var/run/docker.sock │
|
||
│ tgit-runner (скрипт на хосте) ─┤ │
|
||
│ ▲ ssh, forced command ▼ │
|
||
│ │ tgit-svc-<service> контейнеры (песочница) │
|
||
└─────┼──────────────────────────────────────────────────────────────────────── ┘
|
||
│
|
||
бот (pipeline/), после зелёного CI tgit-uploads ──┘
|
||
```
|
||
|
||
**Репозитории:**
|
||
|
||
| Репозиторий | Что внутри | CI/CD |
|
||
|---|---|---|
|
||
| `tgit` (Gitea → зеркало в SourceCraft) | код бота | push в любую ветку: lint + types + tests; push `main`: + деплой бота на хост |
|
||
| `tgit-uploads` (Gitea, ADR-015/016) | общий репозиторий команды: `services/<name>/…` и любые файлы | push `main`: `check` в Gitea Actions → при успехе бот разворачивает изменённые сервисы через `tgit-runner` |
|
||
|
||
## 2. Компоненты
|
||
|
||
### 2.1 `app/bot` — интерфейс Telegram
|
||
aiogram 3. Главное меню — reply-клавиатура (📝 Коммит · 🩺 Статус · 📜 Логи · 🚨 Алерты · 👥 Участники для админа);
|
||
внутри разделов — инлайн-кнопки, сообщение раздела редактируется на месте.
|
||
|
||
- **Middleware доступа** — первым в цепочке, на `message` и `callback_query`: пускает только участников; исключение —
|
||
`/start <token>`. Неавторизованным — одна фраза раз в сутки, остальное игнорируется.
|
||
- **FSM** — `aiogram` storage поверх Postgres (своя реализация `BaseStorage`, таблица `fsm_state`) → состояние переживает рестарт.
|
||
- **callback_data ≤ 64 байт** — в кнопки не кладём пути: `CallbackData` с коротким id, а соответствие id → путь
|
||
хранится в данных FSM текущего экрана.
|
||
|
||
### 2.2 `app/access` — приглашения и участники
|
||
- `create_invite(admin) -> (link, png)`: `secrets.token_urlsafe(16)` → в БД `sha256(token)`, `expires_at`, `created_by`;
|
||
QR — библиотека `segno` (PNG в памяти).
|
||
- `redeem(token, tg_user)`: хэш → поиск → не истёк, не использован → в транзакции `used_by/used_at` + создание участника.
|
||
- Антиперебор: счётчик неудачных попыток по `tg_id` за час (таблица `access_attempts`).
|
||
- `revoke(user)`: `is_active = false`, удаление черновика, сброс FSM.
|
||
|
||
### 2.3 `app/repo` — дерево, черновики, пуш
|
||
- **Локальный клон** `tgit-uploads` в volume. Перед открытием браузера — `git fetch` (не чаще раза в 10 с);
|
||
дерево — `git ls-tree origin/main <path>`, содержимое — `git show origin/main:<path>`. Быстро и без API.
|
||
- **Черновик** (`drafts` + `draft_changes`): `base_sha` (HEAD при начале), список изменений `add|modify|delete` с
|
||
содержимым. Виртуальные новые папки живут только в FSM, пока в них не добавлен файл.
|
||
- **Валидация** (`repo/validate.py`): имя файла, размер, UTF-8, NUL, `ast.parse` для `.py`, запрет `.sourcecraft/` и служебных файлов.
|
||
- **Publisher** — единственная очередь (`asyncio.Queue`, один воркер), чтобы пуши не конфликтовали:
|
||
`fetch` → проверка конфликта (`git diff --name-only base_sha origin/main` ∩ пути черновика) → `reset --hard origin/main`
|
||
→ применить изменения → `commit` с трейлером `Telegram-User` → `push`; отказ push → `pull --rebase` (до 3 раз).
|
||
git — через `asyncio.create_subprocess_exec` (аргументы списком), `GIT_SSH_COMMAND` с ключом бота.
|
||
|
||
### 2.4 `app/sourcecraft` — клиент API
|
||
`httpx.AsyncClient`, Bearer-токен, ретраи на сетевые ошибки и 5xx (экспоненциально, до 5 попыток),
|
||
pydantic-модели ответов. Методы: `list_runs`, `get_run`, `get_cube_logs` (постранично), `run_commit_sha` — sha из лога куба `check/meta` (у прогона в API нет коммита, ADR-014). Подробности — [`dev/SOURCECRAFT_API.md`](dev/SOURCECRAFT_API.md).
|
||
|
||
### 2.5 `app/pipeline` — наблюдатель CI/CD
|
||
CI репозитория загрузок — Gitea Actions (`.gitea/workflows/check.yml`), CD — сам бот через `tgit-runner` (ADR-016).
|
||
|
||
```
|
||
push ─► pushes(ci=pending, cd=pending|skipped) ─► wake()
|
||
PipelineWatcher: CIProvider.find(sha) каждые 10 с (бэкофф до 60 с при ошибках API)
|
||
├ pending/running ─► правка сообщения о push
|
||
├ success/skipped ─► «✅ CI пройден» ─► Deployer (tgit-runner) ─► 🚀 / ⛔ + алерт ⛔ всем
|
||
├ failed ─► «❌ CI упал на шаге …» + хвост лога (длинный — ещё и файлом), CD не запускается
|
||
└ нет вердикта 30 мин ─► ⌛ timeout, CD не запускается
|
||
```
|
||
- **Провайдер за протоколом** `CIProvider` (`find(sha)`, `failure_log(run)`): `gitea.py` — у прогона Gitea есть
|
||
`head_sha`, сопоставление прямое; SourceCraft (ADR-014, куб `meta`) подключится второй реализацией. `CI_PROVIDER=none` —
|
||
CI не ждём, сервисы разворачиваются сразу после push.
|
||
- **Состояние — в БД**: `pushes.ci_status/cd_status` и `pipeline_runs` (`ci` — id прогона, ссылка, упавший шаг;
|
||
`cd` — `tgit-runner`, не развёрнутые сервисы). Сообщение о push каждый раз рендерится из БД. После рестарта бот
|
||
продолжает: ждёт CI дальше, а прерванный деплой (`cd=running`) повторяет.
|
||
- **Опрос только по делу**: нет незавершённых push — цикл спит до `wake()` от нового push.
|
||
- **Порядок версий**: прогоны CI могут закончиться не по порядку; старый push не разворачивает сервис, который уже
|
||
развернёт более новый прошедший CI push — иначе откатил бы его.
|
||
- Правка сообщения не уведомляет, поэтому исходы — отдельными сообщениями автору ответом на сообщение о push (C-30…C-32).
|
||
|
||
### 2.6 `app/runtime` — контейнеры и проверки
|
||
Через `docker-socket-proxy` (только `GET`):
|
||
- **Список** — контейнеры с меткой `tgit.managed=true` (сервисы и служебные: bot, postgres, proxy).
|
||
- **Карточка** — `inspect` (state, health, restart count, started_at, метки `tgit.sha`, `tgit.deployed_at`) + `stats`.
|
||
- **Проверки** (`runtime/checks.py`) — набор независимых функций `Check -> CheckResult(ok, detail)`:
|
||
`running`, `docker_health`, `restarts_stable` (сравнение с 5 мин назад, история из БД), `cpu_ok`, `mem_ok`,
|
||
`no_errors_in_logs` (5 мин, шаблон из `config/checks.yaml`), `http_probe` (если у контейнера есть метка
|
||
`tgit.health_url`; пока нет runner (#13) — проба из сети бота). Каждая проверка — со своим таймаутом: упавшая или
|
||
зависшая даёт ❌ с причиной, остальные выполняются. Пороги и окно — в `config/checks.yaml`.
|
||
- **Изоляция** — прокси отдаёт контейнеры всего хоста, поэтому бот сам фильтрует по метке `tgit.managed=true` и
|
||
отказывает в `inspect`/`stats`/`logs` по id не из этого списка ещё до запроса.
|
||
- **Мониторинг** — поток `events` (`die`, `oom`, `health_status`, `restart`) и опрос `stats` раз в 15 с → события для `alerts`.
|
||
|
||
### 2.7 `app/logs` — grep-движок
|
||
- `parse_grep(expr) -> GrepQuery` — подмножество синтаксиса grep: шаблон, `-i`, `-v`, `-E`, `-c`, `-C N`; всё
|
||
остальное — ошибка с подсказкой. Разбор `shlex.split`, **никакого исполнения**.
|
||
- `run(query, source)` — источник `runtime.logs(container, since, tail)`; поиск модулем `regex` с таймаутом на
|
||
строку; лимиты: шаблон ≤ 200, вход ≤ 5 МБ, вывод ≤ 500 строк.
|
||
- `to_command(query)` — человекочитаемый `grep …` для показа пользователю.
|
||
|
||
### 2.8 `app/agent` — LLM-агент для логов
|
||
```
|
||
промт ─► prefilter ─✗─► отказ + алерт 🛡
|
||
│✓
|
||
▼
|
||
guard (LLM, JSON {verdict, reason}) ─ attack / невалидный JSON ─► отказ + алерт 🛡
|
||
│safe
|
||
▼
|
||
planner (LLM, JSON LogQuery: container, since, pattern, flags, limit) ─ невалидно ─► «не смог построить запрос»
|
||
│
|
||
▼
|
||
validate (pydantic, белый список контейнеров, компиляция regex, анализ бэктрекинга)
|
||
│
|
||
▼
|
||
logs.run(query) ─► ответ: grep-команда + строки
|
||
```
|
||
- `prefilter.py` — детерминированные правила из `config/agent_rules.yaml` (длина, шаблоны атак).
|
||
- `guard.py` и `planner.py` — **отдельные** вызовы с узкими системными промптами (`config/prompts/`); в planner
|
||
передаются только промт пользователя и список контейнеров — **логи в LLM не попадают** (ADR-007, ADR-013).
|
||
- Провайдер — `LLMProvider` + фабрика (ADR-008); в тестах `mock`.
|
||
- Журнал: `agent_requests` (промт, вердикт, запрос, токены); выполненный поиск — ещё и в `log_queries` (`mode=agent`).
|
||
- Квота — 30 вопросов на участника за скользящие сутки, считается по `agent_requests`.
|
||
- Недоступный LLM — «попробуйте позже», а не атака; невалидный ответ стража — атака; невалидный ответ планировщика —
|
||
«не смог построить запрос».
|
||
- Алерт 🛡 пока отправляется администраторам напрямую; с #15 он станет алертом `agent_attack` в общем журнале.
|
||
- При `LLM_PROVIDER=mock` агент выключен: mock отвечает стражу `{}`, и каждый вопрос считался бы атакой.
|
||
|
||
### 2.9 `app/alerts` — алерты
|
||
- **Детекторы** (`detectors.py`) — чистые функции «наблюдение за раунд + история снапшотов → условия, которые
|
||
выполняются сейчас»: 💥 краш (выход ≠ 0), 🧠 OOM, 🔁 ≥ 3 рестарта за 10 мин, 🩺 unhealthy, 🔥 CPU > 80 % или
|
||
RAM > 90 % на протяжении минуты, 💾 < 10 % свободного места на разделе тома бота, 🔌 Docker API недоступен > 5 мин.
|
||
Пороги — `config/alerts.yaml`. Монитор (`monitor.py`) вызывается после каждого раунда снапшотов (15 с).
|
||
- **По состоянию, а не по событиям**: сервис сверяет условия с открытыми алертами — новое открывает алерт, пропавшее
|
||
закрывает. Пропущенное событие не теряет алерт, а бесконечный рестарт остаётся одним алертом.
|
||
- **Дедупликация** (`service.py`): ключ `kind + service`. Условие, вернувшееся в течение 10 мин после закрытия, снова
|
||
открывает тот же алерт с `count += 1`: исходные сообщения редактируются, новые не шлются.
|
||
- **Разовые события** (🛡 атака через агента; ⛔ отказ при деплое — от `Deployer`) открываются сразу
|
||
закрытыми; повтор в окне 10 мин — тоже счётчик.
|
||
- **Рассылка** (`broadcast.py`) — всем активным участникам, 🛡 — только администраторам; ~25 сообщений/с,
|
||
`RetryAfter` пережидается, заблокировавший бота участник пропускается. `alert_deliveries` хранит
|
||
`chat_id`/`message_id`, чтобы обновлять счётчик и отвечать «✅ восстановлено» на исходное сообщение.
|
||
- **Сообщение**: тип, сервис, время, хвост лога (30 строк), кнопки «📜 Логи», «🩺 Статус», «📋 Список алертов».
|
||
Кнопки открывают экран новым сообщением, чтобы не затирать алерт.
|
||
- **Список**: активные сверху, затем история за 7 дней, по 10 на страницу; 🛡 в общий список не попадает.
|
||
- **Если Docker недоступен**, оцениваются только 🔌 и 💾: алерты контейнеров не закрываются вслепую.
|
||
- **Краш при restart-политике**: контейнер поднимается за секунду и между раундами не виден в `exited` — поэтому
|
||
краш распознаётся и по росту `RestartCount` относительно предыдущего раунда; каждый новый — счётчик.
|
||
- **Гистерезис**: алерт закрывается, когда условия нет `resolve_after` (60 с), а правки со счётчиком — не чаще раза
|
||
в минуту: флапающий сервис не заваливает чат.
|
||
|
||
### 2.10 `app/db` — хранилище
|
||
SQLAlchemy 2 (async) + Alembic. PostgreSQL в Compose; в тестах — SQLite.
|
||
|
||
| Таблица | Поля (основные) |
|
||
|---|---|
|
||
| `users` | `tg_id` PK, `username`, `full_name`, `role` (`member`/`admin`), `is_active`, `invited_by`, `joined_at`, `last_seen_at` |
|
||
| `invites` | `id`, `token_hash` UNIQUE, `created_by`, `created_at`, `expires_at`, `used_by`, `used_at` |
|
||
| `access_attempts` | `tg_id`, `at`, `ok` |
|
||
| `fsm_state` | `key` PK, `state`, `data` JSON |
|
||
| `drafts` | `id`, `user_id` UNIQUE, `base_sha`, `message`, `created_at`, `expires_at` |
|
||
| `draft_changes` | `id`, `draft_id`, `path`, `op` (`add`/`modify`/`delete`), `content` BYTEA |
|
||
| `pushes` | `id`, `user_id`, `sha`, `message`, `paths` JSON, `chat_id`, `message_id`, `ci_status`, `cd_status`, `created_at` |
|
||
| `pipeline_runs` | `id`, `push_id`, `stage` (`ci`/`cd` — задачи одного workflow `check-and-deploy`), `run_slug`, `status`, `failed_cube`, `url`, `updated_at` |
|
||
| `container_snapshots` | `container`, `at`, `restart_count`, `cpu_pct`, `mem_pct`, `health` — для проверок «за 5 мин» (TTL 1 сутки) |
|
||
| `alerts` | `id`, `kind`, `service`, `payload` JSON, `count`, `opened_at`, `last_at`, `resolved_at` |
|
||
| `alert_deliveries` | `alert_id`, `chat_id`, `message_id` |
|
||
| `log_queries` | `id`, `user_id`, `container`, `mode` (`tail`/`grep`/`agent`), `query` JSON, `lines`, `at` |
|
||
| `agent_requests` | `id`, `user_id`, `prompt`, `verdict`, `reason`, `log_query` JSON, `tokens`, `at` |
|
||
|
||
### 2.11 Конфигурация
|
||
`app/config.py` — `pydantic-settings`, класс на подсистему (`TelegramSettings`, `AccessSettings`, `SourceCraftSettings`,
|
||
`GitSettings`, `DockerSettings`, `LLMSettings`, `AlertSettings`), всё из `.env`. Пороги, тексты, правила, промпты — `config/*.yaml`.
|
||
|
||
## 3. Потоки данных
|
||
|
||
### 3.1 Вход по приглашению
|
||
```
|
||
админ ─ «Пригласить» ─► access.create_invite ─► QR (PNG) + ссылка
|
||
новый пользователь ─ скан QR ─► t.me/<bot>?start=<token> ─► /start <token> ─► redeem ─✓─► users + главное меню
|
||
└✗─► отказ (+ счётчик попыток)
|
||
```
|
||
|
||
### 3.2 Коммит → CI → CD
|
||
```
|
||
📝 Коммит ─► дерево origin/main ─► файл/папка ─► изменения в черновик ─► 💬 сообщение ─► 🚀 Push ─► подтверждение
|
||
│
|
||
Publisher (очередь) ─ fetch, конфликт?, commit, push ──► tgit-uploads@main
|
||
│ SourceCraft CI
|
||
«a1b2c3d ⏳ CI» ◄──────────────────────────────────────────────────────────────────────────┘ Gitea Actions check: compile · ruff · bandit · pytest
|
||
PipelineWatcher ─ poll runs(sha) ─► «✅ CI» ─► «🚀 CD: weather…» ─► «🚀 CD: weather развёрнут» CD: бот → ssh tgit-runner deploy <svc> <sha>
|
||
└► ❌ + лог шага автору; ⛔ CD → алерт всем
|
||
```
|
||
|
||
### 3.3 Логи через агента
|
||
См. §2.8; ответ — grep-команда + найденные строки; запись в `agent_requests` и `log_queries`.
|
||
|
||
### 3.4 Алерт
|
||
`runtime`/`pipeline`/хост → детектор → дедупликация → рассылка всем участникам → … → условие прошло → «✅ восстановлено».
|
||
|
||
## 4. Песочница сервисов (`tgit-runner`)
|
||
|
||
`infra/runner/tgit-runner` — bash-скрипт на хосте; единственная команда, доступная ключам CI и бота (`command="…"` в
|
||
`authorized_keys` пользователя `tgit-deploy`). Подкоманды: `deploy <service> <sha>`, `restart <service>`,
|
||
`probe <service> <path>` (HTTP-проба изнутри сети сервиса). Аргументы валидируются: имя `^[a-z][a-z0-9-]{1,30}$`, sha — hex.
|
||
|
||
`deploy`: получает архив `services/<service>` в stdin — его делает бот (`git archive <sha>:services/<service>`)
|
||
после зелёного CI (§2.5), так что у хоста нет доступа к репозиторию → проверяет архив (только файлы и папки, без `..`,
|
||
≤ 20 МБ, есть `main.py`) → собирает образ `tgit-svc-<service>:<sha>` из
|
||
шаблона (`python:3.12-slim` + `requirements.txt`, `HEALTHCHECK` из `tgit.yaml` сервиса, если задан) → запускает:
|
||
|
||
```
|
||
docker run -d --name tgit-svc-<service> --label tgit.managed=true --label tgit.service=<service>
|
||
--label tgit.sha=<sha> --label tgit.deployed_at=<iso>
|
||
--user 10001:10001 --read-only --tmpfs /tmp:size=16m
|
||
--cap-drop ALL --security-opt no-new-privileges
|
||
--memory 128m --memory-swap 128m --cpus 0.25 --pids-limit 64
|
||
--network none # или tgit-svc-egress, если сеть разрешена администратором
|
||
--restart on-failure:3
|
||
--log-opt max-size=5m --log-opt max-file=2
|
||
```
|
||
Новая версия стартует рядом со старой как `tgit-svc-<service>-next` и заменяет её, только став healthy (с
|
||
`health_url` — по `HEALTHCHECK`, без него — 10 с без рестартов). Иначе новый контейнер удаляется, старый продолжает
|
||
работать, код выхода 3 → бот пишет автору push «⛔ не развёрнут» с хвостом лога и поднимает алерт ⛔ всей команде.
|
||
Сервис, папка которого исчезла из коммита, удаляется (`remove`). Перезапуск — `restart` из «Статуса» (админ).
|
||
Установка и обновление — [`infra/runner/README.md`](../infra/runner/README.md).
|
||
|
||
## 5. CI/CD
|
||
|
||
### 5.1 Репозиторий бота (`tgit/.sourcecraft/ci.yaml`)
|
||
|
||
Код живёт в Gitea, SourceCraft получает его push-зеркалом и запускает CI/CD (ADR-010).
|
||
|
||
| Триггер | Workflow | Шаги |
|
||
|---|---|---|
|
||
| push в любую ветку (через зеркало Gitea; PR живут в Gitea — ADR-010) | `check` | `uv sync --frozen` → `ruff check` + `ruff format --check` → `mypy app` → `pytest --cov=app --cov-fail-under=70` |
|
||
| push в `main` | `check` + `deploy` | → `infra/deploy/deploy.sh` по SSH (секреты `DEPLOY_SSH_KEY`, `DEPLOY_HOST`) → smoke: healthcheck контейнера |
|
||
|
||
### 5.2 Репозиторий загрузок (`HSE/tgit-uploads`, `.gitea/workflows/check.yml`)
|
||
Шаблон хранится в `tgit/infra/uploads-template/` и ставится в `tgit-uploads` один раз (ADR-016).
|
||
|
||
| Что | Шаги |
|
||
|---|---|
|
||
| `check` (Gitea Actions, push в `main`) | `compileall` → `ruff check` (мягкий набор правил) → `bandit -ll` → `pytest` для сервисов с тестами; первый упавший шаг бот показывает автору |
|
||
| CD (бот, после зелёного `check`) | изменённые `services/<name>/` → `git archive` → `ssh tgit-deploy@host deploy <name> <sha>` для каждого |
|
||
|
||
`.sourcecraft/ci.yaml` в шаблоне остаётся для возврата к SourceCraft.
|
||
|
||
## 6. Деплой
|
||
|
||
| Где | Как |
|
||
|---|---|
|
||
| Любая машина | `git clone` → `cp .env.example .env` → заполнить → `make up` |
|
||
| VPS vobot (основной) | push в `main` → CI → `infra/deploy/deploy.sh` → `/srv/tgit` (пользователь `tgit-deploy`, Compose-проект `tgit`) |
|
||
|
||
`deploy.sh` (по образцу meditron): `git archive` → `scp` → распаковка в `app.new` → перенос `.env` → атомарная замена
|
||
`app` → `docker compose -p tgit up -d --build` → ожидание healthcheck → запись `REVISION`. Все SSH-шаги с ретраями
|
||
(хост режет часть соединений). Хост задаётся `DEPLOY_HOST` — тот же скрипт работает для любого сервера.
|
||
|
||
Бюджет ресурсов на VPS (свободно ~1,9 ГБ RAM): бот ≤ 256 МБ, postgres ≤ 256 МБ, proxy ≤ 32 МБ,
|
||
сервисы ≤ 10 × 128 МБ.
|
||
|
||
## 7. Структура репозитория
|
||
|
||
```
|
||
app/ код бота: bot, access, repo, sourcecraft, pipeline, runtime, logs, agent, alerts, db, config.py, main.py
|
||
config/ alerts.yaml, checks.yaml, agent_rules.yaml, texts.yaml, prompts/
|
||
infra/
|
||
deploy/ deploy.sh, docker-compose.server.yml
|
||
runner/ tgit-runner, шаблон Dockerfile сервиса, authorized_keys.example
|
||
uploads-template/ .sourcecraft/ci.yaml, ruff.toml, README для tgit-uploads
|
||
examples/ демо-сервисы: ok, syntax error, lint fail, crash, cpu hog, unhealthy, escape attempts
|
||
tests/ pytest: unit + интеграционные с фейками
|
||
docs/ SPEC, ARCHITECTURE, DECISIONS, dev/, презентация
|
||
Dockerfile · docker-compose.yml · Makefile · pyproject.toml · .env.example
|
||
```
|
||
|
||
## 8. Что важно учесть
|
||
|
||
| Риск | Что делаем |
|
||
|---|---|
|
||
| Случайные люди и спам | Только одноразовые приглашения с TTL; middleware на каждом апдейте; антиперебор токенов; лимиты пушей и запросов |
|
||
| Утечка QR | Одноразовость + 24 ч; админ видит, кто вошёл по какому приглашению, и может отозвать доступ |
|
||
| Участник ломает CI из бота | Запрет правок `.sourcecraft/` и служебных файлов (C-16) |
|
||
| Чужой код на одном сервере с прод-Vobot | Песочница §4, лимиты, сеть выключена; общий лимит 10 сервисов; в перспективе — отдельный хост (ADR-005) |
|
||
| Docker-сокет = root на хосте | Бот — только через read-only proxy; запуск — через `tgit-runner` с forced command |
|
||
| Пользователь `tgit-deploy` в группе `docker` ≈ root | Ключ ограничен одной командой; `tgit-runner` валидирует аргументы |
|
||
| Атака через агента (injection, exfiltration) | Фильтр → LLM-страж → структурированный `LogQuery` → валидация; логи в LLM не передаются; алерт админам |
|
||
| ReDoS в regex (свой grep и от LLM) | Модуль `regex` с таймаутом, лимит длины шаблона и объёма логов |
|
||
| Утечка секретов | Секреты не логируются и не идут в LLM; маскирование в выводе P2 |
|
||
| Нестабильный SourceCraft API | Ретраи, идемпотентность, проверка перед повторным POST |
|
||
| Гонки пушей и конфликты | Одна очередь Publisher; проверка изменений с `base_sha`; `pull --rebase` при отказе |
|
||
| Лимиты Telegram (4096 символов, 64 байта callback_data, ~30 msg/s) | Длинное — файлом; короткие id в кнопках; очередь рассылки алертов |
|
||
| Шторм алертов | Дедупликация `kind+service`, счётчик вместо новых сообщений, «восстановлено» ответом |
|
||
| Диск хоста занят на 75 % | Ротация логов контейнеров, `docker image prune` старых ревизий в `tgit-runner`, алерт 💾 |
|