- app/pipeline: протокол CIProvider, клиент Gitea Actions (прогон по head_sha, упавший
шаг, лог джоба, ретраи), PipelineWatcher — опрос только при незавершённых push,
сообщение о push обновляется CI ⏳ → ✅/❌ → CD 🚀/⛔, автору — отдельные уведомления
- деплой сервисов — только после зелёного CI; старый push не откатывает сервис,
который развернёт более новый; после рестарта бот продолжает с того же места
- CI_PROVIDER=none — прежнее поведение (деплой сразу после push)
- шаблон tgit-uploads: .gitea/workflows/check.yml; .gitea/ защищён от правок из бота
- миграция 0002: старые push помечены skipped, чтобы не слать им «таймаут CI»
- ADR-016, ARCHITECTURE §2.5/§5.2, README, DEPLOY, .env.example
Refs #11
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- infra/runner/tgit-runner: deploy/restart/remove/list по SSH forced command;
аргументы не исполняются и не глобятся; архив — только файлы и папки,
без «..», ≤ 20 МБ, есть main.py; health_url — только localhost
- песочница: uid 10001, read-only, tmpfs noexec, cap-drop ALL,
no-new-privileges, 128 МБ без swap, 0.25 CPU, 64 PID, --network none
(сеть — только по egress-allow админа), лимит 10 сервисов, 2 образа
- новая версия стартует рядом (-next) и заменяет старую, только став
healthy; иначе удаляется, старая работает, exit 3
- бот: после push git archive изменённых services/<name>/ → runner;
автору — «🚀 работает» или «⛔ не развёрнут» с логом, команде — алерт ⛔;
удалённая папка → remove; «♻️ Перезапустить» в «Статусе» для админа (S-5)
- examples/escape_attempts: все попытки выбраться — BLOCKED на живом Docker
- install.sh, authorized_keys.example; CD бота обновляет runner на хосте
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Найдено смоуком на живых контейнерах (examples/, #19):
- контейнер с restart: on-failure поднимается за секунду — между раундами
его не видно в exited; краш теперь распознаётся и по росту RestartCount
(сравнение со снапшотами до текущего раунда), повторы — счётчик
- алерт закрывается, только когда условия нет resolve_after (60 с): иначе
флапающий сервис получал бы «✅ восстановлено» после каждого раунда
- правки сообщений со счётчиком — не чаще раза в минуту на алерт
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- детекторы по состоянию после каждого раунда снапшотов: краш, 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>
- prefilter: правила из config/agent_rules.yaml (длина, команды, shell,
секреты и системные файлы, prompt injection) — без LLM
- guard: отдельный вызов, JSON {verdict, reason}; невалидный ответ = attack,
недоступный LLM — «попробуйте позже», а не атака
- planner: только AgentLogQuery (контейнер из белого списка, ≤ 7 д, шаблон
≤ 200 без вложенных квантификаторов, лимит ≤ 500); логи в LLM не идут
- бот: «🤖 Спросить агента» → показывает построенный grep и строки;
атака — вежливый отказ и 🛡 администраторам; квота 30 в сутки
- журнал agent_requests + log_queries (mode=agent)
- при LLM_PROVIDER=mock агент выключен
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- app/logs/grep.py: подмножество grep (шаблон, -i -v -E -c -C N), разбор
shlex без исполнения, BRE → regex, таймаут на строку и бюджет на поиск,
лимиты 200 символов / 500 строк вывода
- app/logs/journal.py: каждый поиск в log_queries (L-6)
- бот: выбор контейнера, хвост 50/200 строк или 5 мин/1 ч/24 ч, свой grep
по 24 ч, длинный результат — файлом .log (L-5)
- логи Docker: не больше 50 000 строк и 5 МБ, сохраняются новейшие
- fix: кнопки меню и /menu, /help больше не проглатываются шагами ввода
(имя файла, содержимое, сообщение коммита, grep) — фильтр free_input
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- docker-socket-proxy в Compose (только GET /containers/*), сокет только у него
- app/runtime/docker.py: read-only клиент; видны только контейнеры с меткой
tgit.managed=true, чужие id отклоняются до запроса
- app/runtime/checks.py: running, health, рестарты за окно, CPU/RAM, ошибки
в логе, HTTP-проба; у каждой проверки свой таймаут, отчёт всегда полный
- app/runtime/snapshots.py: снапшоты раз в 15 с в container_snapshots
- бот: список с 🟢🟡🔴⚪, карточка, «🩺 Запустить проверки», «🔄 Обновить»
- деплой ставит метки tgit.sha / tgit.deployed_at на контейнер бота
- имя джоба CD без неотрисованного выражения
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
При непустом черновике в папке — «🚀 Закоммитить · N»: нет сообщения → спрашивает его и
сразу показывает подтверждение пуша; есть → сразу подтверждение. На подтверждении —
«✏️ Изменить сообщение». Раньше Push был спрятан в «🧾 Черновик» → «💬» → «🚀».
Refs #9
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- make demo-repo: локальный bare-репозиторий загрузок из infra/uploads-template
- make run: бот на этой машине (SQLite, настройки из .env)
- fix: при относительном clone_dir клон уезжал в data/data/uploads (git clone
запускается из родительской папки) — путь теперь абсолютный; тест на это
- data/ в .gitignore
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Publisher: пуши по одному (lock), fetch → проверка конфликта с base_sha → чистое дерево
origin/main → черновик → коммит от tgit-bot с трейлером Telegram-User → push;
отказ push → rebase и повтор (до 3 раз); защита от путей вне клона; лимит 20 пушей/час
- «💬 Сообщение коммита» (1–200 символов, первая строка ≤ 72) и «🚀 Push» с подтверждением;
исходы: запушено (sha, ссылка, ⏳ CI) · конфликт → «Перезаписать» · нечего коммитить · лимит · сбой
- запись в pushes (chat_id, message_id — для обновления статуса CI/CD в #11), черновик очищается
- ruff: RUF001–003 выключены — кириллица в текстах это норма
Refs #9
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- новый файл: имя → содержимое текстом (```-обёртка снимается) или документом
- новая папка (виртуальная, пока в ней нет файлов), замена с diff +N/−M, удаление
- валидация: имя, ≤ 100 КБ, UTF-8, без NUL; .py — ast.parse с выбором «исправить / всё равно»;
.sourcecraft/, .tgit/, ruff.toml из бота не меняются
- DraftService: черновик в БД, 24 ч, ≤ 50 изменений; удаление нового файла убирает его из черновика
- дерево показывает черновик поверх репозитория: 🆕 / ✏️ / 🗑, размер — как будет после пуша
- экран черновика (A/M/D) с «↩️ убрать»
Refs #26
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- UploadsRepo: локальный клон, fetch не чаще fetch_interval, ls-tree -z (пробелы/кириллица),
cat-file, последний коммит по пути; git без shell, ssh с -F /dev/null (без конфига хоста)
- «📝 Коммит»: папки/файлы с размерами, ⬆️ Вверх, страницы по 20, карточка файла
с последним коммитом, «👁 Показать» (<pre> или документом); в кнопках только индексы
- ключ для контейнера: GIT_SSH_KEY_B64 → временный файл 0600 (смонтированный файл хоста
контейнерному uid не прочитать); volume /data для клона
- тесты на настоящем bare-репозитории во временной папке
Refs #8
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- AccessService: одноразовые приглашения (128 бит, в БД только sha256, TTL 24 ч),
атомарный захват приглашения (условный UPDATE — из двух сканов одного QR проходит один),
антиперебор (> 5 неудач в час — молчание), отзыв (черновик и FSM удаляются), повторное приглашение
- ADMIN_IDS заводятся в БД при старте; дальше — только по приглашению
- AccessMiddleware: участники из БД (+ last_seen раз в 5 мин), новичкам — только /start <токен>
- «👥 Участники» для админов: список, QR (segno) + ссылка, отзыв с подтверждением
- один ответ на неверный/истёкший/использованный токен — без подсказок
Refs #25
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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>
- LLMProvider: complete(messages, schema) -> LLMResult (текст, разобранный JSON, токены)
- openai_compat: /chat/completions, structured output через response_format json_schema,
JSON из ```-обёртки тоже разбирается; ретраи на 429/5xx и сетевые ошибки, 4xx — сразу ошибка
- mock: детерминированные правила для тестов и демо без сети
- LLMSettings (LLM_PROVIDER/BASE_URL/MODEL/API_KEY…), CLI python -m app.llm.cli
Refs #17
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- config: pydantic-settings по подсистемам, ADMIN_IDS через запятую
- texts.yaml + валидация при старте (лишний/пропущенный ключ — ошибка сразу)
- меню: reply-клавиатура разделов (Участники — только админам), инлайн «Назад»/«Отмена»
с редактированием сообщения на месте, /start, /menu, /help
- AccessMiddleware (outer) на сообщениях и кнопках: до #25 пускает только ADMIN_IDS,
постороннему — одна фраза в сутки, устаревшие кнопки молча гасятся
- heartbeat-файл + HEALTHCHECK в Dockerfile; JSON-логи
- FSM пока в памяти (фабрика), хранилище в БД — в #5
- тесты на фейковой сессии aiogram через настоящий Dispatcher
Refs #4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>