12 KiB
work_item, stage, author_agent, status, created_at, model_used
| work_item | stage | author_agent | status | created_at | model_used |
|---|---|---|---|---|---|
| ORCH-095 | analysis | analyst | ready-for-review | 2026-06-09 | claude-opus-4-8 |
02 — ТЗ (TRZ): ORCH-095 — HTML-безопасность динамических полей render_task_tracker
Work Item: ORCH-095 · Repo: orchestrator · Стадия: analysis
ТЗ описывает конкретные изменения к реализации, выведенные из BRD и фактического кода. Архитектурное обоснование/выбор точки внесения экранирования — задача архитектора (06-adr).
1. Сводка изменения
Текст live-карточки (render_task_tracker) собирается с parse_mode=HTML из намеренной
разметки-обёртки (<a href> номер задачи, форматирование) и подставляемых данных. Сейчас
экранирован только заголовок (esc_title) и href/label внутри plane_issue_link; остальные
данные вставляются сырыми. Литерал <1м (длительность < 1 мин), возвращаемый _fmt_minutes,
Telegram парсит как открывающий тег → editMessageText падает 400 can't parse entities →
edit_telegram → EDIT_FAILED → update_task_tracker делает ранний return → карточка
застывает.
Требуется: (а) сделать формат «< 1 мин» HTML-безопасным; (б) гарантировать HTML-безопасность всех данных, попадающих в текст карточки, не экранируя намеренную разметку-обёртку; (в) обеспечить возобновление обновлений ранее застрявших карточек. Изменение локализовано в слое уведомлений; машина стадий/гейты/схема БД не затрагиваются.
2. Задействованные модули / пути
| Путь | Действие |
|---|---|
src/notifications.py |
изменить — _fmt_minutes (~280) и/или точки рендера в render_task_tracker (~355): HTML-безопасность данных |
src/notifications.py::render_task_tracker |
изменить — экранировать данные: длительности (dur), status_label, model/effort, метрики (defence-in-depth); НЕ трогать num_html, _done_link-разметку |
src/notifications.py::_card_status_label (~1173) |
проверить/экранировать на потребителе — статус-лейбл вставляется в status_line сырым |
src/notifications.py::edit_telegram (~157) |
возможно изменить (на усмотрение архитектора) — классификация can't parse entities для восстановления застрявших карточек (BR-5/AC-4) |
src/notifications.py::update_task_tracker (~650) |
возможно затронуть — ветка EDIT_FAILED vs пересоздание при перманентном parse-фейле (BR-5/AC-4) |
tests/test_telegram_tracker.py (или новый tests/test_tracker_html_escape.py) |
создать/дополнить — юнит HTML-безопасности всех динамических полей |
CHANGELOG.md |
изменить — запись о фиксе |
Примечание:
fmt_tokens/fmt_cost/short_model_nameживут вsrc/usage.py; их выход сейчас HTML-безопасен (цифры/./k/M/$/имя модели). Менятьsrc/usage.pyне требуется — defence-in-depth экранирование делается на потребителе вnotifications.py.
3. Функциональные требования
FR-1 — HTML-безопасный формат «меньше минуты» (⇒ BR-1, BR-3)
Длительность стадии < 60 с не должна порождать подстроку, которую Telegram трактует как
открывающий тег. Текущий _fmt_minutes(seconds) при 0 < seconds < 60 возвращает литерал
"<1м" (notifications.py:288-289). Поведение должно стать одним из (выбор — архитектор):
- экранированный вывод
<1м(видится оператору как<1м), либо - переформулировка
~0м/< 1 минс последующим экранированием. Инвариант: для любого входа_fmt_minutes(включая0м,Nм,~Nмот_capped_review_str) результат, попав вparse_mode=HTML, не ломает парсер._fmt_minutesсохраняет never-raise (нечисловой/None вход →0м).
FR-2 — HTML-безопасность всех данных карточки (⇒ BR-2)
Каждое подставляемое значение-данные, попадающее в текст render_task_tracker,
экранируется html.escape(...) ровно один раз перед вставкой в HTML-текст. Перечень полей-данных:
| Поле | Источник | Текущий статус |
|---|---|---|
| Заголовок задачи | title → esc_title |
уже экранирован ✓ (не дублировать) |
| Длительности стадий / BRD / done | _fmt_minutes, _capped_review_str |
дыра (FR-1) |
| Статус-лейбл карточки | _card_status_label → status_label |
дыра — экранировать |
| Имя модели | short_model_name(last["model"]) |
экранировать (defence-in-depth) |
| Эффорт | _run_effort(last) |
экранировать (defence-in-depth) |
| Токены / стоимость | fmt_tokens/fmt_cost |
HTML-безопасны; экранировать defence-in-depth |
| Метка «попытка N» / лейблы стадий | статические константы _TRACKER_STAGES/_BRD_LABEL |
статичны; не требуют, но безопасно |
Инвариант FR-2: после рендера ни один символ < > &, пришедший из данных, не остаётся
неэкранированным в выходном тексте.
FR-3 — Сохранность намеренной разметки-обёртки (⇒ BR-4)
Намеренные HTML-фрагменты не экранируются:
num_html=plane_issue_link(...)— кликабельный<a href>номер задачи (внутри уже экранированы href черезhtml.escape(url, quote=True)и label);link_for(...)в строке «⏳ ждёт …» — намеренные ссылки;_done_link(...)— строка🔗 PR #n · 📦 Внедрено. После фикса эти фрагменты рендерятся как валидный HTML и остаются кликабельными. Запрещено двойное экранирование уже экранированных полей (esc_title, внутренностиplane_issue_link).
FR-4 — Возобновление обновлений застрявших карточек (⇒ BR-5)
После деплоя фикса карточка, ранее застрявшая на 400 can't parse entities, должна
возобновить обновления. Достаточное условие по умолчанию: текст следующего рендера больше не
содержит небезопасной подстроки → editMessageText проходит (200) на ближайшем переходе
стадии. Опционально (решение архитектора): классифицировать перманентный parse-фейл в
edit_telegram/update_task_tracker как повод переотправить свежую карточку вместо
тихого return по EDIT_FAILED — но без регресса защиты от дублей (ORCH-087: транзиентные
фейлы по-прежнему НЕ плодят карточки). Если выбирается переклассификация — она должна отличать
перманентный can't parse entities от транзиентного (network/timeout/5xx).
FR-5 — never-raise (⇒ NFR-1)
Все изменённые функции сохраняют контракт «никогда не роняют конвейер»: ошибка
экранирования/рендера → деградация к существующему fallback (f"task-{task_id}" /
пропуск строки), не исключение наружу.
4. Изменения API
Нет. HTTP-эндпоинты не добавляются/не меняются. (Внешний вызов — только исходящий
editMessageText/sendMessage к Telegram Bot API; контракт вызова не меняется, меняется
лишь безопасность text.)
5. Изменения схемы БД
Нет. Таблицы tasks/agent_runs/tracker_messages не затрагиваются; миграций нет.
6. Требования к новым/изменённым QG checks
Нет. QG_CHECKS / check_* / STAGE_TRANSITIONS / машинные вердикты не затрагиваются. Баг —
в слое рендера уведомлений, вне Quality Gate.
7. Совместимость / регресс
- Обратная совместимость: изменение чисто в формировании строки текста карточки; данные
БД, схема, режимы трекера (
bump/edit), леджер сирот (ORCH-087), статусная модель (ORCH-066) — без изменений. - Область раската: все проекты на общем прод-инстансе (self-hosting) — фикс применяется к каждому новому рендеру сразу после деплоя; не требует миграции/бэкфилла.
- Kill-switch: не требуется (исправление дефекта корректности, а не новая фича-ветка). Если
архитектор выбирает переклассификацию parse-фейла в
update_task_tracker(FR-4 опц.) — оценить целесообразность флага; по умолчанию изменение поведения минимально и безопасно. - Обратимость: изменение откатывается обычным revert PR (только
notifications.py+ тесты + CHANGELOG); прод-контейнер не требует ручных операций над данными. - Артефакты pipeline: обновляются
12-review.md(reviewer),13-test-report.md(tester),06-adr/ADR-001-*.md(архитектор — выбор точки экранирования и стратегии FR-4),CHANGELOG.md. Машинные вердикты гейтов — без изменений. - Self-hosting: обязательна стадия
deploy-staging(8501) перед прод-деплоем; продorchestratorне рестартуется в рамках разработки.