Files
ElebobaandClaude Opus 5.5 e8c3a321a3
CI / ruff · mypy · pytest (pull_request) Successful in 1h3m54s
feat(access): демо-QR — многоразовое приглашение для показа
- invites.max_uses/uses: обычное приглашение — 1 вход, демо-QR — до DEMO_INVITE_MAX_USES (50),
  живёт DEMO_INVITE_TTL_HOURS (3 ч); захват атомарный, UPDATE … WHERE uses < max_uses
- users.guest: вошедшие по демо-QR; «🧹 Отключить гостей демо» отзывает всех и гасит демо-QR
- «👥 Участники»: кнопка «🎤 Демо-QR», гости в списке — 🎟
- миграция 0003 (ADD COLUMN без batch — SQLite не пересоздаёт users); проверена на Postgres
- SPEC A-3a, ADR-017, DEMO.md, .env.example

Refs #55

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 18:11:53 +03:00

22 KiB
Raw Permalink Blame History

Архитектурные решения (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 (как 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 берёт триггеры CI из основной ветки → основная ветка tgit в SourceCraft — dev (в Gitea остаётся main); иначе CI включался бы только после продвижения в main.
  • Токен SourceCraft хранится в настройках зеркала Gitea → нужен отдельный токен с доступом только к tgit.
  • Issues #1–#21 перенесены из SourceCraft с теми же номерами; в SourceCraft закрыты со ссылкой.

ADR-011: Доступ — только по одноразовым QR-приглашениям

Дата: 2026-10-13 · Статус: принято

Контекст: бот коммитит в общий репозиторий и запускает код на сервере — случайный человек, нашедший бота, не должен ничего сделать или заспамить. Решение: приглашение = deep link t.me/<bot>?start=<token> в виде 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/<name>/; у каждого участника свой черновик; алерты рассылаются всем (кроме 🛡 — только админам). Альтернативы: проекты с владельцами (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.

ADR-014: Связь «прогон ↔ коммит» — через куб meta, печатающий sha

Дата: 2026-10-14 · Статус: принято (уточняет ADR-003)

Контекст: бот должен понять, какой прогон CI относится к его пушу. В API SourceCraft у прогона нет sha коммита (ни в списке, ни в деталях), фильтра по коммиту нет; логи служебных кубов (#git-clone) API не отдаёт. Решение: первый куб задачи check в tgit-uploads — meta: echo "TGIT_COMMIT_SHA=$SOURCECRAFT_COMMIT_SHA". Бот читает его лог (SourceCraftClient.run_commit_sha) и сопоставляет прогон с пушем. Альтернативы: запускать CI из бота вручную (POST /cicd/runs с head.commit) — номер прогона известен сразу, но push-триггер пришлось бы убрать, и ручные пуши в репозиторий остались бы без CI; сопоставление по времени — ломается при параллельных пушах. Последствия: +1 короткий куб на прогон; куб meta нельзя удалять или переименовывать (он в .sourcecraft/, который бот защищает от правок); прогоны старше этого решения sha не имеют.

ADR-015: CI/CD бота — на Gitea Actions (как в Vobot)

Дата: 2026-10-16 · Статус: принято (меняет ADR-010 в части CI/CD бота)

Контекст: SourceCraft отложен: ключ бота там не принимается, триггеры зависят от основной ветки, раннеры с очередью до минут. Гейт и деплой нужны сейчас. Решение: CI (.gitea/workflows/ci.yml: ruff, mypy, миграции на Postgres, pytest) и CD (.gitea/workflows/deploy.yml → infra/deploy/deploy.sh) — на Gitea Actions, как у Vobot и meditron, на общих раннерах Gitea. Деплой — на хост Vobot, dev/prod по веткам, через пользователя tgit-deploy. Репозиторий загрузок на сервере — временно HSE/tgit-uploads в Gitea. Альтернативы: ждать SourceCraft — блокирует проверку и демо. Последствия: .sourcecraft/ci.yaml бота остаётся для будущего возврата; при возврате к SourceCraft меняются GIT_UPLOADS_URL и ключ, код не трогаем. На RF-сервере Telegram — только через прокси (TELEGRAM_PROXY_URL).

ADR-016: CI репозитория загрузок — Gitea Actions; CD — бот после зелёного CI

Дата: 2026-10-19 · Статус: принято (меняет ADR-003/014 в части репозитория загрузок; SourceCraft — при возврате)

Контекст: после ADR-015 репозиторий загрузок живёт в Gitea (HSE/tgit-uploads), а CI у него не было: бот разворачивал сервис сразу после push, проверив только синтаксис, и «⏳ CI» в сообщении не обновлялся (#11). SourceCraft по-прежнему не принимает ключ бота (#7). Решение: CI загрузок — Gitea Actions (.gitea/workflows/check.yml: compile · ruff · bandit · pytest). Бот опрашивает Gitea API (/actions/runs?head_sha=…) — у прогона есть sha, куб meta не нужен. CD остаётся у бота: после зелёного check он отправляет изменённые сервисы в tgit-runner, как в #13. Наблюдатель зависит от протокола CIProvider, вендорский код — в app/pipeline/gitea.py. Альтернативы: SourceCraft — блокирован ключом бота; деплой из самого workflow (ssh на хост из раннера Gitea) — ключ к tgit-runner пришлось бы отдать общим раннерам Gitea, а бот всё равно должен знать исход деплоя для автора и алертов; не ждать CI — ruff/bandit/тесты из CI ничего бы не решали. Последствия: боту нужен токен с чтением Actions репозитория загрузок (CI_TOKEN); без CI_PROVIDER поведение прежнее (деплой сразу). Деплой ждёт CI: на общих раннерах это от ~30 с до минут при очереди, таймаут — 30 мин. Возврат к SourceCraft — вторая реализация CIProvider поверх готового SourceCraftClient (ADR-014), код наблюдателя не меняется.

ADR-017: Демо-QR — многоразовое приглашение с лимитом для показа

Дата: 2026-10-22 · Статус: принято (дополняет ADR-011)

Контекст: на защите зал должен попробовать бота по одному QR с экрана; одноразовое приглашение (ADR-011) пускает только первого (#55). Решение: у приглашения — лимит входов max_uses (обычное — 1) и счётчик uses; захват атомарный (UPDATE … WHERE uses < max_uses). Админ выпускает демо-QR отдельной кнопкой: DEMO_INVITE_MAX_USES входов (50), DEMO_INVITE_TTL_HOURS (3 ч). Вошедшие помечаются users.guest; «Отключить гостей демо» отзывает их всех и гасит действующие демо-QR, чтобы фото QR больше никого не пускало. Альтернативы: выпустить 50 одноразовых QR — неудобно показывать; роль «гость» без коммита — больше работы, а код гостей и так исполняется только в песочнице tgit-runner (ADR-005/006). Последствия: риск ADR-011 «одна утечка открывает доступ всем» ограничен лимитом, коротким сроком и массовым отзывом. Гости получают алерты и дневной лимит агента наравне с участниками.