Files
tgit/docs/ARCHITECTURE.md
T
ElebobaandClaude Opus 5.5 6c6f766abc
CI / ruff · mypy · pytest (pull_request) Successful in 4m49s
feat(alerts): алерты всей команде — детекторы, дедупликация, список (#15)
- детекторы по состоянию после каждого раунда снапшотов: краш, OOM,
  цикл рестартов, unhealthy, перегрузка ≥ 1 мин, мало места, Docker
  недоступен; пороги в config/alerts.yaml
- AlertService: ключ kind+service, повтор в 10 мин — счётчик в исходном
  сообщении, «✅ восстановлено» ответом; разовые события (🛡) со счётчиком
- рассылка всем активным (🛡 — только админам), темп ~25/с, RetryAfter,
  заблокировавший бота пропускается; alert_deliveries для правок и ответов
- «🚨 Алерты»: активные + 7 дней по 10, карточка; кнопки «📜 Логи»,
  «🩺 Статус» под алертом открывают новое сообщение, а не затирают алерт
- 🛡 атаки через агента идут через общий сервис алертов
- убраны заглушки «Раздел в разработке» — у всех разделов свои роутеры

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 15:36:09 +03:00

26 KiB
Raw Blame History

Архитектура tgit

Требования — в SPEC.md (v2), решения и их обоснования — в 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-<service> контейнеры (песочница)           │
              └─────┼──────────────────────────────────────────────────────────────────────── ┘
                    │
      SourceCraft CI (tgit-uploads): check → deploy ──┘

Репозитории:

Репозиторий Что внутри CI/CD
tgit (Gitea → зеркало в SourceCraft) код бота push в любую ветку: lint + types + tests; push main: + деплой бота на хост
tgit-uploads (SourceCraft) общий репозиторий команды: services/<name>/… и любые файлы push main: check → при успехе deploy изменённых сервисов через 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.

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 — алерты

  • Детекторы (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: исходные сообщения редактируются, новые не шлются.
  • Разовые события (🛡 атака через агента; 📦 CD и ⛔ отказ после деплоя — после #11/#13) открываются сразу закрытыми; повтор в окне 10 мин — тоже счётчик.
  • Рассылка (broadcast.py) — всем активным участникам, 🛡 — только администраторам; ~25 сообщений/с, RetryAfter пережидается, заблокировавший бота участник пропускается. alert_deliveries хранит chat_id/message_id, чтобы обновлять счётчик и отвечать «✅ восстановлено» на исходное сообщение.
  • Сообщение: тип, сервис, время, хвост лога (30 строк), кнопки «📜 Логи», «🩺 Статус», «📋 Список алертов». Кнопки открывают экран новым сообщением, чтобы не затирать алерт.
  • Список: активные сверху, затем история за 7 дней, по 10 на страницу; 🛡 в общий список не попадает.
  • Если Docker недоступен, оцениваются только 🔌 и 💾: алерты контейнеров не закрываются вслепую.

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» ◄──────────────────────────────────────────────────────────────────────────┘ check: py_compile · ruff · pytest · bandit
   PipelineWatcher ─ poll runs(sha) ─► «✅ CI» ─► «⏳ CD» ─► «🚀 CD: weather»       deploy: 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> по sha из tgit-uploads → собирает образ 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

Если новый контейнер не стал 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/<name>/ → ssh tgit-deploy@host deploy <name> <sha> для каждого

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, алерт 💾