Files
tgit/docs/ARCHITECTURE.md
T
ElebobaandClaude Opus 5.5 c016b47a8a feat(bot): клиент SourceCraft API — прогоны, логи кубов, sha коммита, ретраи
- SourceCraftClient (httpx): list_runs, get_run, get_cube_logs (постранично), run_commit_sha;
  ретраи на сетевые ошибки и 5xx с экспоненциальной задержкой, 4xx — SourceCraftError без повторов
- модели: RunStatus/EventType из swagger, «1970-01-01» = не задано, failed_cubes без служебных #-кубов
- у прогона в API нет sha → куб check/meta в tgit-uploads печатает TGIT_COMMIT_SHA (ADR-014)
- CLI: python -m app.sourcecraft.cli runs
- тесты на реальных ответах API (фикстуры), порог покрытия 45 → 80

Refs #10

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:02:52 +03:00

23 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 (если в services/<name>/tgit.yaml объявлен health_url; проба из сети сервиса через runner). Результат — отчёт-таблица.
  • Мониторинг — поток 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 (промт, вердикт, запрос, число строк, токены).

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, workflow (check/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, алерт 💾