# Архитектура tgit > Требования — в [`SPEC.md`](SPEC.md), решения и их обоснования — в [`DECISIONS.md`](DECISIONS.md). ## 1. Общая схема ``` Telegram ◄──long polling──┐ │ ┌────────────┴─────────────────────────── хост (VPS / ноутбук) ─────────────┐ │ tgit-bot (один процесс, asyncio) │ │ ├─ bot/ aiogram: команды, загрузка файлов, кнопки │ │ ├─ uploads/ валидация → GitPublisher (очередь, git + SSH) ───────────┼──► SourceCraft │ ├─ ci/ CIWatcher: polling прогонов по sha, логи кубов ◄────────┼─── REST API │ ├─ runtime/ Monitor: события и метрики контейнеров, логи │ │ ├─ alerts/ правила, антиспам, доставка │ │ └─ llm/ LogAssistant: вопрос → LogQuery → поиск → ответ ─────────┼──► LLM API │ │ │ │ │ ┌────┴─────┐ ┌────────┴────────┐ │ │ │ postgres │ │ docker-proxy │ (только чтение: ps/logs/ │ │ └──────────┘ │ socket-proxy │ events/stats) │ │ └────────┬────────┘ │ │ │ /var/run/docker.sock │ │ tgit-runner (скрипт на хосте) ─┤ │ │ ▲ ssh, forced command ▼ │ │ │ tgit-app- контейнеры (песочница) │ └─────┼─────────────────────────────────────────────────────────────────────┘ │ SourceCraft CI (tgit-uploads): check → deploy ──┘ ``` **Два репозитория:** | Репозиторий | Что внутри | CI/CD | |---|---|---| | `tgit` | код бота | PR/push `dev`: lint + types + tests; push `main`: + деплой бота на хост | | `tgit-uploads` | `projects//…` от пользователей | push `main`: проверки проекта; успех → `ssh tgit-runner deploy ` | ## 2. Компоненты ### 2.1 `app/bot` — интерфейс Telegram aiogram 3, роутеры по фичам (`upload`, `projects`, `logs`, `ask`, `admin`). Middleware: проверка доступа по белому списку, логирование апдейтов без содержимого файлов. Длинные ответы → файл. ### 2.2 `app/uploads` — загрузка и коммит - `validate(file) -> ValidatedUpload` — размер, кодировка, `ast.parse`, режим `# tgit: mode=…`. - `GitPublisher` — держит локальный клон `tgit-uploads` в volume; **единственная очередь** (`asyncio.Queue`, один воркер), чтобы пуши не конфликтовали. Шаги: `fetch` → `reset --hard origin/main` → запись файла → `commit` → `push`; при отказе push — повтор с `pull --rebase` (до 3 раз). git вызывается через `asyncio.create_subprocess_exec` с `GIT_SSH_COMMAND="ssh -i -o IdentitiesOnly=yes"`. - Пустой diff → «изменений нет», коммита нет. ### 2.3 `app/sourcecraft` — клиент API `httpx.AsyncClient`, Bearer-токен, ретраи на сетевые ошибки и 5xx (экспоненциально, до 5 попыток), типизированные ответы (pydantic). Методы: `list_runs`, `get_run`, `get_cube_logs`, `trigger_workflow`. Подробности API — [`dev/SOURCECRAFT_API.md`](dev/SOURCECRAFT_API.md). ### 2.4 `app/ci` — наблюдатель за CI Фоновая задача. Опрашивает `/cicd/runs` **только пока есть незавершённые загрузки** (интервал 10 с, бэкофф до 60 с при ошибках). Сопоставляет прогон с загрузкой по sha коммита, на смене статуса редактирует сообщение пользователя. При падении скачивает лог упавшего куба. Таймаут прогона — 30 мин → статус `timeout`. Webhooks у SourceCraft нет — отсюда polling (ADR-003). ### 2.5 `app/runtime` — мониторинг приложений Через `docker-socket-proxy` (только `GET` на containers/logs/events/stats): - поток `events` с фильтром `label=tgit.project` → `die` (код выхода), `oom`, `restart`; - раз в 15 с `stats` по контейнерам приложений → CPU% / RAM% от лимита, скользящее окно 1 мин; - `logs` с `since`/`tail` — для `/logs` и LLM. Управление (`stop`/`restart`) бот **не** делает через сокет — он вызывает `tgit-runner` (ADR-006). ### 2.6 `app/alerts` — алерты Правила (пороги — в `config/alerts.yaml`) → событие → антиспам (ключ `project+kind`, окно 10 мин, счётчик) → доставка владельцу и подписчикам. Системные алерты (недоступен API/Docker/БД) — администраторам. ### 2.7 `app/llm` — ассистент по логам ``` вопрос ──► LLM #1 (JSON-схема LogQuery) ──► валидация ──► поиск по логам ──► маскирование ──► LLM #2 ──► ответ ``` - `LogQuery`: `source` (`app` | `ci`), `since` (≤ 7 д), `pattern` (regex ≤ 200 символов), `level`, `limit` (≤ 500). - Поиск — Python `regex` с таймаутом (защита от ReDoS), **никакого shell** (ADR-007). - Маскирование: токены, `KEY=…`, `password=…`, JWT, e-mail, IP → `***`. - Провайдер — за протоколом `LLMProvider` + фабрика по конфигу: OpenAI-совместимый API (YandexGPT, OpenAI, локальная модель) и `mock` для тестов (ADR-008). ### 2.8 `app/db` — хранилище SQLAlchemy 2 (async) + Alembic. PostgreSQL в Compose; в тестах — SQLite. | Таблица | Поля (основные) | |---|---| | `users` | `tg_id` PK, `username`, `role` (`user`/`admin`), `is_allowed`, `llm_quota_used`, `created_at` | | `projects` | `id`, `name` UNIQUE, `owner_id` → users, `mode` (`service`/`job`), `network_enabled`, `alerts_enabled` | | `uploads` | `id`, `project_id`, `user_id`, `filename`, `sha`, `status` (`committed`/`ci_running`/`ci_ok`/`ci_failed`/`deployed`/`timeout`), `chat_id`, `message_id`, `created_at` | | `ci_runs` | `id`, `upload_id`, `run_slug`, `status`, `failed_cube`, `url`, `updated_at` | | `alerts` | `id`, `project_id`, `kind`, `payload` JSON, `count`, `first_at`, `last_at`, `delivered_at` | | `llm_requests` | `id`, `user_id`, `project_id`, `question`, `log_query` JSON, `tokens`, `created_at` | ### 2.9 Конфигурация `app/config.py` — `pydantic-settings`, один класс на подсистему (`TelegramSettings`, `SourceCraftSettings`, `DockerSettings`, `LLMSettings`, …), всё из `.env`. Пороги и тексты — `config/*.yaml`. ## 3. Потоки данных ### 3.1 Загрузка → CI → CD ``` user ──.py──► bot ── validate ──✗──► «ошибка, строка N» │✓ ▼ GitPublisher ──push──► tgit-uploads@main │ │ (SourceCraft CI) «✅ a1b2c3d, ⏳ CI» ◄┘ ├─ check: py_compile · ruff · pytest · bandit └─ deploy: ssh tgit-deploy@host → tgit-runner deploy │ CIWatcher ──poll runs(sha)──► статус ──► edit message ▼ docker build + run tgit-app- Monitor ◄── events/stats ── docker-proxy ◄──────────────────┘ └──► alerts ──► user ``` ### 3.2 Вопрос к логам `/ask` → квота → LLM #1 → `LogQuery` → валидация → `runtime.logs()` / `sourcecraft.get_cube_logs()` → фильтр regex → маскирование → LLM #2 (вопрос + найденные строки) → ответ + запрос + доказательства → запись в `llm_requests`. ## 4. Песочница приложений (`tgit-runner`) `infra/runner/tgit-runner` — bash-скрипт на хосте; единственная команда, доступная ключу CI (`command="…"` в `authorized_keys` пользователя `tgit-deploy`). Подкоманды: `deploy `, `stop `, `restart `. `deploy`: забирает архив проекта по sha из `tgit-uploads` → собирает образ `tgit-app-:` из шаблона (`python:3.12-slim` + `requirements.txt`) → запускает: ``` docker run -d --name tgit-app- --label tgit.project= --label tgit.sha= --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-apps-egress, если сеть разрешена --restart on-failure:3 # service; для job — без рестарта --log-opt max-size=5m --log-opt max-file=2 ``` ## 5. CI/CD ### 5.1 Репозиторий бота (`tgit/.sourcecraft/ci.yaml`) | Триггер | Workflow | Шаги | |---|---|---| | PR в `dev`/`main`, push в `dev` | `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` один раз. Определение изменённого проекта — `git diff --name-only HEAD~1 -- projects/` внутри куба. ## 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, uploads, sourcecraft, ci, runtime, alerts, llm, db, config.py, main.py config/ alerts.yaml, texts.yaml, llm-промпты infra/ deploy/ deploy.sh, docker-compose.server.yml runner/ tgit-runner, шаблон Dockerfile приложения, authorized_keys.example uploads-template/ .sourcecraft/ci.yaml и README для tgit-uploads examples/ демо-скрипты для загрузки (ok, syntax error, lint fail, crash, cpu hog) tests/ pytest: unit + интеграционные с фейками docs/ SPEC, ARCHITECTURE, DECISIONS, dev/, презентация Dockerfile · docker-compose.yml · Makefile · pyproject.toml · .env.example ``` ## 8. Что важно учесть | Риск | Что делаем | |---|---| | Чужой код на одном сервере с прод-Vobot | Песочница §4, лимиты, сеть выключена; общий лимит 10 приложений; в перспективе — отдельный хост (ADR-005) | | Docker-сокет = root на хосте | Бот — только через read-only proxy; запуск — через `tgit-runner` с forced command | | Пользователь `tgit-deploy` в группе `docker` ≈ root | Ключ ограничен одной командой; `tgit-runner` валидирует аргументы (`^[a-z][a-z0-9-]{1,30}$`, sha hex) | | Prompt injection через логи | LLM только возвращает `LogQuery`, который валидируется; ответ LLM — просто текст пользователю | | ReDoS в regex от LLM | Модуль `regex` с таймаутом, лимит длины паттерна и объёма логов | | Утечка секретов в LLM | Маскирование перед отправкой; секреты бота не попадают в логи | | Нестабильный SourceCraft API | Ретраи, идемпотентность, проверка перед повторным POST | | Гонки пушей | Одна очередь GitPublisher, `pull --rebase` при отказе | | Лимиты Telegram (4096 символов, ~30 msg/s) | Длинное — файлом; редактирование одного сообщения вместо потока | | Диск хоста занят на 75% | Ротация логов контейнеров, `docker image prune` старых ревизий приложений в `tgit-runner` |