# Архитектурные решения (ADR) Формат записи: ``` ## ADR-NNN: <заголовок> **Дата:** YYYY-MM-DD · **Статус:** принято | отменено | заменено ADR-MMM **Контекст:** почему возник вопрос. **Решение:** что выбрали. **Альтернативы:** что отвергли и почему. **Последствия:** что это меняет. ``` --- ## ADR-001: Хостинг кода и задач — SourceCraft **Дата:** 2026-10-12 · **Статус:** заменено ADR-010 **Контекст:** нужен репозиторий проекта, трекер задач и CI. **Решение:** код бота, issues и CI — на SourceCraft. Загрузки пользователей — в отдельный репозиторий там же. **Альтернативы:** GitHub/Gitea — не соответствует постановке преподавателя. **Последствия:** CI описывается в формате SourceCraft; бот ходит в SourceCraft по своему deploy-ключу. ## ADR-002: Telegram — long polling, а не webhook **Дата:** 2026-10-12 · **Статус:** принято **Контекст:** бот должен легко разворачиваться «где угодно», включая ноутбук без белого IP. **Решение:** long polling (aiogram `start_polling`). **Альтернативы:** webhook — нужен домен, HTTPS и nginx-vhost; выигрыш в задержке для учебной нагрузки несущественен. **Последствия:** входящих портов нет; на VPS не нужен vhost. Один экземпляр бота на токен. ## ADR-003: Статус CI — polling SourceCraft API **Дата:** 2026-10-12 · **Статус:** принято **Контекст:** нужно сообщать пользователю результат CI. **Решение:** бот опрашивает `/cicd/runs` и сопоставляет прогон с коммитом по sha; опрос идёт только пока есть незавершённые загрузки. **Альтернативы:** исходящие webhooks — у SourceCraft не задокументированы; «CI сам пишет в Telegram» — секрет бота в чужом CI и нет связи с исходным сообщением. **Последствия:** задержка статуса до ~10 с; клиент API обязан переживать обрывы (ретраи). ## ADR-004: Образы собираются на хосте, реестр — опционально **Дата:** 2026-10-12 · **Статус:** принято **Контекст:** у SourceCraft есть container registry (`pkg.sourcecraft.tech`), но его создание требует платёжного аккаунта Yandex Cloud и роли администратора организации. **Решение:** и бот, и приложения собираются `docker build` прямо на хосте (как meditron). Переменная `REGISTRY` в будущем переключит деплой на `pull` готового образа. **Альтернативы:** registry SourceCraft — зависимость от биллинга; GHCR/Docker Hub — ещё один внешний аккаунт. **Последствия:** сборка нагружает хост (на время деплоя); деплой не зависит от внешнего реестра. ## ADR-005: Приложения пользователей — на том же хосте, в песочнице **Дата:** 2026-10-12 · **Статус:** принято (временно) **Контекст:** единственный доступный сервер — VPS vobot, где работают прод и dev Vobot и meditron. **Решение:** запускать на нём же, но в жёсткой песочнице (non-root, read-only, cap-drop ALL, лимиты CPU/RAM/PIDs, без сети по умолчанию) и с общим лимитом 10 приложений. **Альтернативы:** отдельный VPS — правильнее по изоляции, но лишняя машина для учебного проекта; self-hosted worker SourceCraft — запуск внутри CI не даёт долгоживущих сервисов. **Последствия:** риск побега из контейнера ложится на прод-Vobot. Хост приложений вынесен в конфиг (`RUNNER_HOST`) — переезд на отдельную машину без изменений кода. ## ADR-006: Запуск и управление приложениями — через `tgit-runner` с forced command **Дата:** 2026-10-12 · **Статус:** принято **Контекст:** и CI, и бот должны запускать/останавливать контейнеры; полный доступ к Docker равен root на хосте. **Решение:** единая точка — скрипт `tgit-runner` на хосте; SSH-ключи CI и бота в `authorized_keys` пользователя `tgit-deploy` ограничены `command="tgit-runner"`; скрипт валидирует аргументы. Бот читает состояние через read-only docker-socket-proxy. **Альтернативы:** смонтировать docker.sock в бота — компрометация бота = root на хосте. **Последствия:** вся логика песочницы — в одном месте и тестируется отдельно. ## ADR-007: LLM строит структурированный запрос, а не shell-команду **Дата:** 2026-10-12 · **Статус:** принято, дополнено ADR-013 **Контекст:** идея — «LLM формирует grep по логам». Логи содержат текст, подконтрольный пользователю (prompt injection). **Решение:** LLM возвращает JSON `LogQuery` (источник, период, regex, уровень, лимит) по схеме; бот валидирует и выполняет поиск сам, в Python, с таймаутом regex. Пользователю показывается эквивалентный grep. **Альтернативы:** выполнять `grep` от LLM в shell — инъекции команд, ReDoS, доступ к файлам хоста. **Последствия:** поиск ограничен возможностями `LogQuery`, зато безопасен и тестируем. ## ADR-008: LLM-провайдер — за протоколом, по умолчанию OpenAI-совместимый API **Дата:** 2026-10-12 · **Статус:** принято **Контекст:** провайдер не выбран; нужны тесты без сети. **Решение:** протокол `LLMProvider` + фабрика по `LLM_PROVIDER`: `openai_compat` (`LLM_BASE_URL`, `LLM_MODEL`, `LLM_API_KEY` — подходит YandexGPT, OpenAI, локальная модель) и `mock`. **Альтернативы:** привязка к одному вендору — как в Vobot, правило «провайдер за протоколом». **Последствия:** выбор конкретной модели — настройка, а не код. ## ADR-009: Стек **Дата:** 2026-10-12 · **Статус:** принято **Решение:** Python 3.12, uv, aiogram 3, httpx, pydantic-settings, SQLAlchemy 2 async + Alembic, PostgreSQL 16, `regex`, pytest (+ pytest-asyncio, respx), ruff, mypy. Контейнеризация — Docker Compose. **Альтернативы:** python-telegram-bot — синхронное наследие в API; SQLite в проде — проще, но Postgres ближе к реальным сервисам и уже привычен по Vobot/meditron. **Последствия:** тесты на SQLite через тот же SQLAlchemy. ## ADR-010: Gitea — основной репозиторий и трекер, SourceCraft — зеркало + CI/CD **Дата:** 2026-10-12 · **Статус:** принято (заменяет ADR-001) **Контекст:** веб-интерфейс SourceCraft работает нестабильно и часть функций отображает плохо; API периодически рвёт соединения. При этом CI/CD на SourceCraft — часть постановки преподавателя. **Решение:** код, issues, PR и ревью — в Gitea [`HSE/tgit`](https://gitea.24studbank.ru/HSE/tgit) (как Vobot/meditron). Встроенный push-mirror Gitea синхронизирует репозиторий в SourceCraft на каждый push (+ раз в 8 ч); CI/CD бота и репозиторий загрузок `tgit-uploads` остаются в SourceCraft. **Альтернативы:** только SourceCraft — неудобно работать; только Gitea (Gitea Actions) — уходим от постановки; зеркало по SSH — на этом Gitea SSH-зеркала отключены, поэтому HTTPS + токен SourceCraft. **Последствия:** - В SourceCraft ничего не пушим и не мержим — зеркало перезаписывает его целиком. - Триггер `pull_request` в SourceCraft не срабатывает (PR живут в Gitea) → CI бота запускается на `push` во все ветки. - Токен SourceCraft хранится в настройках зеркала Gitea → нужен отдельный токен с доступом только к `tgit`. - Issues #1–#21 перенесены из SourceCraft с теми же номерами; в SourceCraft закрыты со ссылкой. ## ADR-011: Доступ — только по одноразовым QR-приглашениям **Дата:** 2026-10-13 · **Статус:** принято **Контекст:** бот коммитит в общий репозиторий и запускает код на сервере — случайный человек, нашедший бота, не должен ничего сделать или заспамить. **Решение:** приглашение = deep link `t.me/?start=` в виде QR; токен 128 бит, одноразовый, 24 ч, в БД — только хэш. Проверка доступа — middleware на каждом апдейте (сообщения и кнопки). Неавторизованным — одна фраза в сутки, дальше игнор; антиперебор 5 попыток/час. Первые админы — `ADMIN_IDS` в `.env`. **Альтернативы:** белый список Telegram ID вручную — неудобно (надо узнавать ID); пароль в `/start` — передаётся и утекает как текст; многоразовый QR — одна утечка открывает доступ всем. **Последствия:** нужен раздел «Участники» для админов (выпуск QR, список, отзыв); библиотека `segno` для QR. ## ADR-012: Командная модель — общий репозиторий, общие сервисы, алерты всем **Дата:** 2026-10-13 · **Статус:** принято (заменяет «проекты с владельцами» из SPEC v1) **Контекст:** v2 требует, чтобы алерты получали все авторизованные, а коммит шёл интерактивно по дереву репозитория. **Решение:** все участники равноправно работают с одним репозиторием загрузок (`main`) и видят все сервисы; сервис = папка `services//`; у каждого участника свой черновик; алерты рассылаются всем (кроме 🛡 — только админам). **Альтернативы:** проекты с владельцами (v1) — противоречит «алерты всем» и усложняет навигацию правами. **Последствия:** права проще (участник/админ); риск, что участник сломает чужой сервис, — принимаем: история коммитов с автором в трейлере, CI-гейт, откат (P2). ## ADR-013: Агент логов — фильтр, LLM-страж и планировщик раздельно; логи в LLM не передаются **Дата:** 2026-10-13 · **Статус:** принято **Контекст:** промт пользователя может быть попыткой сломать или взломать систему; логи содержат текст, подконтрольный сервисам (injection). **Решение:** конвейер из пяти шагов (SPEC §4.3.1): детерминированный фильтр → отдельный LLM-вызов «страж» с JSON-вердиктом `safe|attack` (невалидный ответ = `attack`) → отдельный LLM-вызов «планировщик», который получает только промт и список контейнеров и возвращает `LogQuery` → валидация → выполнение в Python. Пользователю показывается итоговый grep и найденные строки. Атака → отказ + алерт админам. **Альтернативы:** один вызов LLM с инструкцией «откажись, если атака» — страж и исполнитель в одном контексте, injection обходит оба; передавать логи в LLM для поиска — injection из логов и утечка секретов. **Последствия:** два LLM-вызова на запрос (латентность, токены → квота 30/сутки); даже при обходе стража вред ограничен схемой `LogQuery`.