Files
ElebobaandClaude Opus 5.5 e63698ffe8
CI / ruff · mypy · pytest (pull_request) Successful in 1m47s
docs: презентация и сценарий защиты
- docs/presentation.pdf (12 слайдов): задача → разделы → архитектура → коммит/CI/CD →
  песочница → LLM-агент → алерты → безопасность → качество → демо → итоги;
  исходник docs/presentation/slides.html, пересборка — make slides
- docs/DEMO.md: подготовка, 16 шагов по сценариям SPEC §6 с таймингом и репликами,
  запасной план (нет сети / CI в очереди / нет LLM / нет сервера)
- ссылки в README и CLAUDE.md

Refs #20

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:31:17 +03:00

377 lines
24 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>tgit — git в Телеграме</title>
<!-- Исходник docs/presentation.pdf. Пересобрать: make slides (нужен Chromium) -->
<style>
@page { size: 1280px 720px; margin: 0; }
:root {
--ink: #1d2433; --muted: #5b6475; --line: #d8dde6; --bg: #f6f7fa;
--accent: #2563eb; --ok: #15803d; --bad: #b91c1c; --warn: #b45309; --soft: #e8effd;
}
* { box-sizing: border-box; }
html, body { margin: 0; font-family: "Noto Sans", "DejaVu Sans", sans-serif; color: var(--ink); }
.slide {
width: 1280px; height: 720px; padding: 56px 72px; position: relative; overflow: hidden;
page-break-after: always; background: #fff; display: flex; flex-direction: column;
}
.slide:last-child { page-break-after: auto; }
h1 { font-size: 40px; margin: 0 0 8px; letter-spacing: -0.5px; }
h2 { font-size: 32px; margin: 0 0 28px; letter-spacing: -0.3px; }
h2 small { font-size: 18px; color: var(--muted); font-weight: 400; margin-left: 12px; }
p, li { font-size: 21px; line-height: 1.45; }
ul { margin: 0; padding-left: 26px; } li { margin-bottom: 8px; }
.muted { color: var(--muted); }
.num { position: absolute; right: 40px; bottom: 28px; font-size: 14px; color: var(--muted); }
code, .mono { font-family: "Noto Sans Mono", "DejaVu Sans Mono", monospace; font-size: 0.88em; }
code { background: var(--bg); padding: 1px 6px; border-radius: 4px; }
.cols { display: grid; grid-template-columns: 1fr 1fr; gap: 40px; flex: 1; }
.cols3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; }
.cols4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; }
.card { border: 1px solid var(--line); border-radius: 12px; padding: 20px 22px; background: #fff; }
.card h3 { margin: 0 0 10px; font-size: 21px; }
.card p, .card li { font-size: 17px; margin: 0 0 6px; }
.tag { display: inline-block; font-size: 14px; padding: 2px 10px; border-radius: 999px; background: var(--soft); color: var(--accent); margin: 0 6px 6px 0; }
/* title */
.title { justify-content: center; background: linear-gradient(135deg, #f8faff 0%, #eef3ff 100%); }
.title h1 { font-size: 64px; }
.title .sub { font-size: 26px; color: var(--muted); margin-bottom: 48px; }
.title .meta { font-size: 18px; color: var(--muted); }
/* flow diagrams */
.flow { display: flex; align-items: stretch; gap: 0; margin: 8px 0 24px; }
.step { flex: 1; border: 1px solid var(--line); border-radius: 10px; padding: 14px 14px; background: #fff; font-size: 16px; line-height: 1.35; }
.step b { display: block; font-size: 17px; margin-bottom: 4px; }
.arrow { width: 34px; display: flex; align-items: center; justify-content: center; color: var(--muted); font-size: 22px; flex: none; }
.step.hl { border-color: var(--accent); background: var(--soft); }
.step.ok { border-color: #86efac; background: #f0fdf4; }
.step.bad { border-color: #fca5a5; background: #fef2f2; }
/* architecture */
.arch { position: relative; flex: 1; }
.box { position: absolute; border: 1px solid var(--line); border-radius: 10px; background: #fff; padding: 10px 14px; font-size: 15px; line-height: 1.35; }
.box b { font-size: 16px; }
.zone { position: absolute; border: 2px dashed #c7d2fe; border-radius: 16px; }
.zone span { position: absolute; top: -13px; left: 16px; background: #fff; padding: 0 8px; font-size: 14px; color: var(--accent); }
svg.wires { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
table { border-collapse: collapse; width: 100%; font-size: 17px; }
th, td { text-align: left; padding: 9px 12px; border-bottom: 1px solid var(--line); vertical-align: top; }
th { color: var(--muted); font-weight: 600; font-size: 15px; }
.big { font-size: 46px; font-weight: 700; color: var(--accent); line-height: 1; }
.stat { text-align: left; }
.stat .lbl { font-size: 16px; color: var(--muted); margin-top: 6px; }
.msg { font-family: "Noto Sans Mono", "DejaVu Sans Mono", monospace; font-size: 15px; background: #f1f5f9; border-radius: 10px; padding: 14px 16px; line-height: 1.6; white-space: pre; }
</style>
</head>
<body>
<!-- 1 -->
<section class="slide title">
<h1>tgit — git в Телеграме</h1>
<div class="sub">Команда коммитит код из Telegram, а бот проверяет его, разворачивает<br>в песочнице, следит за сервисами и отвечает на вопросы по логам</div>
<div class="meta">Проект курса «Python» · НИУ ВШЭ · 2026</div>
</section>
<!-- 2 -->
<section class="slide">
<h2>Задача</h2>
<div class="cols">
<div>
<p><b>Исходная постановка</b></p>
<ul>
<li>бот загружает <code>.py</code>-файлы в отдельный репозиторий;</li>
<li>push автоматически запускает CI/CD;</li>
<li>развёртывание на VPS или локально.</li>
</ul>
<p style="margin-top:22px"><b>Согласованные расширения</b></p>
<ul>
<li>статус CI/CD в чате;</li>
<li>алерты о падениях и перегрузке;</li>
<li>удобные логи и LLM-помощник по ним.</li>
</ul>
</div>
<div>
<p><b>Что важно при этом не сломать</b></p>
<ul>
<li>случайный человек, нашедший бота, не должен ничего сделать;</li>
<li>чужой код запускается на общем сервере;</li>
<li>промт к LLM и сами логи — недоверенный ввод;</li>
<li>внешние API (Telegram, Gitea, SourceCraft) рвут соединения.</li>
</ul>
<p class="muted" style="margin-top:22px">Безопасность — не отдельный пункт, а требование к каждому разделу.</p>
</div>
</div>
<div class="num">2</div>
</section>
<!-- 3 -->
<section class="slide">
<h2>Что умеет бот <small>вход только по одноразовому QR</small></h2>
<div class="cols4" style="flex:1">
<div class="card"><h3>📝 Коммит</h3>
<p>Дерево репозитория, новые файлы текстом или документом, замена, удаление.</p>
<p>Черновик, сообщение, push с проверкой конфликтов.</p>
<p>Статус CI → CD прямо в сообщении о push.</p></div>
<div class="card"><h3>🩺 Статус</h3>
<p>Все контейнеры: 🟢 🟡 🔴 ⚪.</p>
<p>Карточка: health, аптайм, рестарты, CPU/RAM от лимита, ревизия.</p>
<p>7 проверок с таймаутами, перезапуск (админ).</p></div>
<div class="card"><h3>📜 Логи</h3>
<p>Хвост за период, свой <code>grep -i -v -E -c -C</code> — в Python, не в shell.</p>
<p>🤖 Вопрос словами: LLM строит запрос, бот ищет сам.</p></div>
<div class="card"><h3>🚨 Алерты</h3>
<p>💥 краш · 🧠 OOM · 🔁 рестарты · 🩺 unhealthy · 🔥 перегрузка · ⛔ деплой · 💾 диск · 🔌 Docker · 🛡 атака.</p>
<p>Всем участникам, без спама, «✅ восстановлено» ответом.</p></div>
</div>
<p class="muted" style="margin-top:20px">👥 Участники (админ): выпуск QR, список, мгновенный отзыв доступа.</p>
<div class="num">3</div>
</section>
<!-- 4 -->
<section class="slide">
<h2>Архитектура</h2>
<div class="arch">
<div class="zone" style="left:0; top:14px; width:760px; height:520px"><span>сервер (VPS) · Docker Compose</span></div>
<div class="box" style="left:24px; top:40px; width:712px; height:236px; background:#f8faff; border-color:#c7d2fe">
<b>tgit-bot</b> <span class="muted">— один процесс asyncio · aiogram 3</span>
<div class="cols3" style="gap:10px; margin-top:12px">
<div class="card" style="padding:10px 12px"><b>bot/</b><br><span class="muted">меню, FSM в БД, роутеры, middleware доступа</span></div>
<div class="card" style="padding:10px 12px"><b>repo/ · pipeline/</b><br><span class="muted">черновики, push, наблюдатель CI → CD</span></div>
<div class="card" style="padding:10px 12px"><b>runtime/ · alerts/</b><br><span class="muted">состояние, проверки, детекторы, рассылка</span></div>
<div class="card" style="padding:10px 12px"><b>logs/ · agent/</b><br><span class="muted">grep-движок, фильтр → страж → планировщик</span></div>
<div class="card" style="padding:10px 12px"><b>access/</b><br><span class="muted">QR-приглашения, антиперебор</span></div>
<div class="card" style="padding:10px 12px"><b>llm/</b><br><span class="muted">провайдер за протоколом</span></div>
</div>
</div>
<div class="box" style="left:24px; top:300px; width:150px"><b>PostgreSQL</b><br><span class="muted">FSM, черновики, алерты</span></div>
<div class="box" style="left:196px; top:300px; width:230px"><b>docker-socket-proxy</b><br><span class="muted">только GET: список, inspect, stats, logs</span></div>
<div class="box" style="left:448px; top:300px; width:288px"><b>tgit-runner</b> <span class="muted">(ssh, forced command)</span><br><span class="muted">deploy · restart · remove — единственный способ запустить код</span></div>
<div class="box" style="left:448px; top:440px; width:288px; border-color:#fca5a5; background:#fef2f2"><b>tgit-svc-*</b><br><span class="muted">сервисы команды в песочнице</span></div>
<div class="box" style="left:820px; top:30px; width:340px"><b>Telegram</b><br><span class="muted">long polling — без входящих портов</span></div>
<div class="box" style="left:820px; top:130px; width:340px"><b>Gitea</b><br><span class="muted">tgit-uploads: код команды · Actions: CI</span></div>
<div class="box" style="left:820px; top:230px; width:340px"><b>LLM API</b> <span class="muted">(OpenAI-совместимый)</span><br><span class="muted">только промт и список контейнеров</span></div>
<div class="box" style="left:820px; top:350px; width:340px; background:var(--bg)"><b>Репозиторий бота</b><br><span class="muted">Gitea Actions: ruff · mypy · pytest → CD на сервер</span></div>
<svg class="wires" viewBox="0 0 1136 542" preserveAspectRatio="none">
<defs><marker id="a" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#94a3b8"/></marker></defs>
<g stroke="#94a3b8" stroke-width="1.5" fill="none" marker-end="url(#a)">
<path d="M736 70 L818 60"/><path d="M736 160 L818 160"/><path d="M736 250 L818 262"/>
<path d="M592 276 L592 298"/><path d="M592 392 L592 436"/><path d="M310 276 L310 298"/>
</g>
</svg>
</div>
<div class="num">4</div>
</section>
<!-- 5 -->
<section class="slide">
<h2>Коммит → CI → CD</h2>
<div class="flow">
<div class="step"><b>Черновик</b>проверка имени, UTF-8, 100 КБ, <code>ast.parse</code></div><div class="arrow">→</div>
<div class="step"><b>Push</b>одна очередь, конфликты с базой черновика, трейлер автора</div><div class="arrow">→</div>
<div class="step hl"><b>CI · Gitea Actions</b>compile · ruff · bandit · pytest</div><div class="arrow">→</div>
<div class="step hl"><b>PipelineWatcher</b>прогон по <code>head_sha</code>, опрос 10 с, только пока есть кого ждать</div><div class="arrow">→</div>
<div class="step ok"><b>CD · tgit-runner</b>только после зелёного CI</div>
</div>
<div class="cols">
<div>
<div class="msg">✅ Запушено: a1b2c3d — feat: кэш
CI: ✅ пройден
https://gitea…/actions/runs/5225
CD: 🚀 hello развёрнуто</div>
<p class="muted" style="font-size:17px; margin-top:12px">Сообщение о push редактируется на месте; исходы — отдельными сообщениями автору: правка не присылает уведомление.</p>
</div>
<div>
<ul>
<li>❌ CI — упавший шаг и хвост лога, длинный — файлом; CD не запускается;</li>
<li>состояние в БД — после рестарта бот продолжает с того же места;</li>
<li>CI закончились не по порядку — старый push не откатит сервис;</li>
<li>CI за протоколом <code>CIProvider</code>: SourceCraft — вторая реализация.</li>
</ul>
</div>
</div>
<div class="num">5</div>
</section>
<!-- 6 -->
<section class="slide">
<h2>Песочница для чужого кода</h2>
<div class="cols">
<div>
<div class="msg">docker run tgit-svc-hello
--user 10001:10001 --read-only
--tmpfs /tmp:size=16m,noexec
--cap-drop ALL
--security-opt no-new-privileges
--memory 128m --memory-swap 128m
--cpus 0.25 --pids-limit 64
--network none</div>
<p class="muted" style="font-size:17px; margin-top:12px">Сеть — только если её просит сервис <b>и</b> разрешил админ на хосте. Не больше 10 сервисов.</p>
</div>
<div>
<ul>
<li>бот <b>не видит</b> docker.sock: чтение — через read-only прокси, запуск — только <code>tgit-runner</code>;</li>
<li>SSH-ключ бота прибит к одной команде (<code>command=…,restrict</code>), аргументы валидируются;</li>
<li>код везёт бот: <code>git archive</code> в stdin — у хоста нет доступа к репозиторию;</li>
<li>новая версия заменяет старую, только став healthy — иначе ⛔ и алерт команде.</li>
</ul>
<p style="margin-top:16px"><span class="tag">examples/escape_attempts</span> запись в FS, интернет, DNS, docker.sock, <code>setuid(0)</code>, raw-сокет, fork-бомба — <b style="color:var(--ok)">все 9 BLOCKED</b></p>
</div>
</div>
<div class="num">6</div>
</section>
<!-- 7 -->
<section class="slide">
<h2>LLM-агент по логам <small>LLM ничего не исполняет</small></h2>
<div class="flow">
<div class="step"><b>1 · Фильтр</b>без LLM: длина, команды, <code>;</code> <code>$(…)</code>, пути к секретам, «игнорируй инструкции»</div><div class="arrow">→</div>
<div class="step hl"><b>2 · Страж</b>отдельный вызов LLM: JSON <code>safe | attack</code>; мусор = атака</div><div class="arrow">→</div>
<div class="step hl"><b>3 · Планировщик</b>отдельный вызов: только JSON <code>LogQuery</code></div><div class="arrow">→</div>
<div class="step"><b>4 · Валидация</b>pydantic, белый список контейнеров, ≤ 7 дней, анализ бэктрекинга</div><div class="arrow">→</div>
<div class="step ok"><b>5 · Поиск</b>бот сам, модуль <code>regex</code> с таймаутом</div>
</div>
<div class="cols">
<div>
<div class="msg">— когда за час были ошибки подключения?
grep -i -E 'connection (refused|reset)'
--since 1h hello
14:02:11 ERROR connection refused …</div>
</div>
<div>
<ul>
<li><b>логи в LLM не передаются</b> — injection из логов невозможен;</li>
<li>атака → вежливый отказ и 🛡 администраторам;</li>
<li>LLM недоступен → «попробуйте позже», не атака;</li>
<li>провайдер за протоколом, в тестах — сценарный фейк; квота 30 в сутки.</li>
</ul>
</div>
</div>
<div class="num">7</div>
</section>
<!-- 8 -->
<section class="slide">
<h2>Алерты без шторма</h2>
<div class="cols">
<div>
<ul>
<li><b>по состоянию, а не по событиям:</b> каждые 15 с детекторы — чистые функции над снапшотами; условие появилось — алерт открыт, пропало — закрыт;</li>
<li><b>дедупликация</b> <code>kind + service</code>: повтор в 10 мин — счётчик в исходном сообщении, не новое;</li>
<li><b>гистерезис:</b> закрытие через 60 с без условия, правки — не чаще раза в минуту;</li>
<li>краш при restart-политике ловится по росту <code>RestartCount</code>;</li>
<li>Docker недоступен — алерты контейнеров не закрываются вслепую.</li>
</ul>
</div>
<div>
<div class="msg">💥 Краш · tgit-svc-crash
14:31 UTC · повторов: 3
Traceback (most recent call last):
…
RuntimeError: boom
[📜 Логи] [🩺 Статус] [📋 Список]</div>
<div class="msg" style="margin-top:14px">↳ ✅ Восстановлено · длилось 4 мин</div>
</div>
</div>
<div class="num">8</div>
</section>
<!-- 9 -->
<section class="slide">
<h2>Безопасность — сводка</h2>
<table>
<tr><th style="width:28%">Угроза</th><th>Защита</th></tr>
<tr><td>Случайный пользователь</td><td>одноразовый QR, 128 бит, 24 ч, в БД только хэш; middleware на <b>каждом</b> апдейте; 5 неудач в час — игнор</td></tr>
<tr><td>Чужой код на сервере</td><td>песочница, лимиты, без сети; запуск только через <code>tgit-runner</code> с forced command</td></tr>
<tr><td>Docker-сокет = root</td><td>у бота только read-only прокси; сам бот отказывает в доступе к чужим контейнерам</td></tr>
<tr><td>Prompt injection</td><td>фильтр + отдельный страж + структурный вывод; grep выполняет Python; логи в LLM не идут</td></tr>
<tr><td>ReDoS</td><td>модуль <code>regex</code> с таймаутом, длина шаблона ≤ 200, анализ вложенных квантификаторов</td></tr>
<tr><td>Поломка CI из бота</td><td><code>.gitea/</code>, <code>.sourcecraft/</code>, <code>.tgit/</code> из Telegram не редактируются</td></tr>
<tr><td>Утечка секретов</td><td>только в <code>.env</code>, <code>SecretStr</code>, не в логах и не в LLM</td></tr>
</table>
<div class="num">9</div>
</section>
<!-- 10 -->
<section class="slide">
<h2>Качество и процесс</h2>
<div class="cols4" style="margin-bottom:34px">
<div class="stat"><div class="big">358</div><div class="lbl">тестов pytest, ~1 мин</div></div>
<div class="stat"><div class="big">92 %</div><div class="lbl">покрытие (порог 80 %)</div></div>
<div class="stat"><div class="big">strict</div><div class="lbl">mypy по всему <code>app/</code> + ruff</div></div>
<div class="stat"><div class="big">16</div><div class="lbl">ADR с альтернативами</div></div>
</div>
<div class="cols">
<ul>
<li>внешние системы — фейки: сессия aiogram, Docker Engine API на <code>httpx.MockTransport</code>, сценарный LLM, respx для Gitea;</li>
<li>сквозной тест «бот → фейковый ssh → настоящий <code>tgit-runner</code> → фейковый docker»;</li>
<li>миграции Alembic up/down/up на PostgreSQL в CI + тест дрейфа моделей.</li>
</ul>
<ul>
<li>задача = issue в Gitea, ветка → PR в <code>dev</code>, ревью и мерж — человек;</li>
<li>CI бота: ruff · mypy · миграции · pytest на каждый PR;</li>
<li>CD: push в <code>dev</code> → деплой на сервер с проверкой health и откатом.</li>
</ul>
</div>
<div class="num">10</div>
</section>
<!-- 11 -->
<section class="slide">
<h2>Демонстрация</h2>
<div class="cols">
<ol style="font-size:20px; line-height:1.55; padding-left:28px; margin:0">
<li>Чужой пишет боту — одна фраза и тишина</li>
<li>QR-приглашение, вход, повторный скан — отказ</li>
<li>Коммит из Telegram → CI ✅ → сервис развёрнут</li>
<li>Коммит с ошибкой ruff → CI ❌ с логом, сервис не тронут</li>
<li>Статус и проверки, свой grep</li>
<li>Вопрос агенту → grep + строки</li>
<li>Атака на агента → отказ, 🛡 админу</li>
<li>Падающий сервис → 💥 всем → ✅ восстановлено</li>
</ol>
<div class="card" style="align-self:start">
<h3>Запасной план</h3>
<p>Нет сети — видео того же сценария.</p>
<p>Нет сервера — <code>make demo-repo · make demo-up · make run</code> на ноутбуке.</p>
<p>Нет LLM — фильтр всё равно отсекает атаку без LLM.</p>
<p class="muted">Подробно: <code>docs/DEMO.md</code></p>
</div>
</div>
<div class="num">11</div>
</section>
<!-- 12 -->
<section class="slide">
<h2>Итоги</h2>
<div class="cols">
<div>
<p><b>Сделано</b></p>
<ul>
<li>все четыре раздела меню и вход по QR;</li>
<li>CI/CD сервисов команды с уведомлениями;</li>
<li>песочница, проверенная попытками побега;</li>
<li>LLM-агент, который не может навредить;</li>
<li>автодеплой самого бота.</li>
</ul>
</div>
<div>
<p><b>Дальше</b></p>
<ul>
<li>CI загрузок в SourceCraft — второй <code>CIProvider</code>;</li>
<li>отдельный сервер под сервисы (ADR-005);</li>
<li>история и откат файла, «🔕 тишина» для алертов;</li>
<li>краткий вывод LLM по найденным строкам с маскированием секретов.</li>
</ul>
<p class="muted" style="margin-top:24px">Код, ТЗ, архитектура, ADR: gitea.24studbank.ru/HSE/tgit</p>
</div>
</div>
<div class="num">12</div>
</section>
</body>
</html>