# Архитектура 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) ────────┼──► SourceCraft │ ├─ pipeline/ PipelineWatcher: CI и CD по sha, уведомления автору ◄────────┼─── REST 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- контейнеры (песочница) │ └─────┼──────────────────────────────────────────────────────────────────────── ┘ │ SourceCraft CI (tgit-uploads): check → deploy ──┘ ``` **Репозитории:** | Репозиторий | Что внутри | CI/CD | |---|---|---| | `tgit` (Gitea → зеркало в SourceCraft) | код бота | push в любую ветку: lint + types + tests; push `main`: + деплой бота на хост | | `tgit-uploads` (SourceCraft) | общий репозиторий команды: `services//…` и любые файлы | push `main`: `check` → при успехе `deploy` изменённых сервисов через `tgit-runner` | ## 2. Компоненты ### 2.1 `app/bot` — интерфейс Telegram aiogram 3. Главное меню — reply-клавиатура (📝 Коммит · 🩺 Статус · 📜 Логи · 🚨 Алерты · 👥 Участники для админа); внутри разделов — инлайн-кнопки, сообщение раздела редактируется на месте. - **Middleware доступа** — первым в цепочке, на `message` и `callback_query`: пускает только участников; исключение — `/start `. Неавторизованным — одна фраза раз в сутки, остальное игнорируется. - **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 `, содержимое — `git show origin/main:`. Быстро и без 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 Фоновая задача. Пока есть незавершённые пуши — опрашивает `/cicd/runs` (10 с, бэкофф до 60 с), сопоставляет прогон с пушем по sha (из куба `meta`, ADR-014). Отслеживает задачи `check` (CI) и `deploy` (CD) workflow `check-and-deploy` отдельно; на смене статуса — редактирует сообщение о пуше и шлёт автору уведомление (успех / падение + лог упавшего куба). Падение `deploy` → событие для `alerts`. Таймаут — 30 мин. Webhooks у SourceCraft нет (ADR-003). ### 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` — алерты - **Детекторы** превращают события runtime/pipeline/хоста в `AlertEvent(kind, service, payload)`; пороги — `config/alerts.yaml`. - **Дедупликация**: ключ `kind + service`; открытый алерт с тем же ключом → `count += 1`, исходные сообщения редактируются (не чаще раза в 10 мин), новые не шлются. - **Рассылка** всем активным участникам (кроме 🛡 — только администраторам) с учётом лимита Telegram (~30 msg/s, очередь отправки); `alert_deliveries` хранит `chat_id`/`message_id` — чтобы обновлять счётчик и отвечать «✅ восстановлено». - **Восстановление**: детектор сообщает, что условие прошло → `resolved_at`, ответ на исходные сообщения. - **Список**: активные + история 7 дней, пагинация. ### 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/?start= ─► /start ─► redeem ─✓─► users + главное меню └✗─► отказ (+ счётчик попыток) ``` ### 3.2 Коммит → CI → CD ``` 📝 Коммит ─► дерево origin/main ─► файл/папка ─► изменения в черновик ─► 💬 сообщение ─► 🚀 Push ─► подтверждение │ Publisher (очередь) ─ fetch, конфликт?, commit, push ──► tgit-uploads@main │ SourceCraft CI «a1b2c3d ⏳ CI» ◄──────────────────────────────────────────────────────────────────────────┘ check: py_compile · ruff · pytest · bandit PipelineWatcher ─ poll runs(sha) ─► «✅ CI» ─► «⏳ CD» ─► «🚀 CD: weather» deploy: ssh → tgit-runner deploy └► ❌ + лог шага автору; ❌ 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 `, `restart `, `probe ` (HTTP-проба изнутри сети сервиса). Аргументы валидируются: имя `^[a-z][a-z0-9-]{1,30}$`, sha — hex. `deploy`: забирает архив `services/` по sha из `tgit-uploads` → собирает образ `tgit-svc-:` из шаблона (`python:3.12-slim` + `requirements.txt`, `HEALTHCHECK` из `tgit.yaml` сервиса, если задан) → запускает: ``` docker run -d --name tgit-svc- --label tgit.managed=true --label tgit.service= --label tgit.sha= --label tgit.deployed_at= --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 ``` Если новый контейнер не стал healthy за 60 с — откат на предыдущий образ и ненулевой код выхода (CD падает → алерт ⛔). ## 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 Репозиторий загрузок (`tgit-uploads/.sourcecraft/ci.yaml`) Шаблон хранится в `tgit/infra/uploads-template/` и ставится в `tgit-uploads` один раз. | Workflow | Шаги | |---|---| | `check` | изменённые файлы (`git diff --name-only HEAD~1`) → `py_compile` · `ruff check` (мягкий набор правил) · `pytest` для затронутых сервисов · `bandit` | | `deploy` (после `check`) | изменённые `services//` → `ssh tgit-deploy@host deploy ` для каждого | ## 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`, алерт 💾 |