Files
orchestrator/docs/work-items/ORCH-095/02-trz.md

12 KiB
Raw Permalink Blame History

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 entitiesedit_telegram → EDIT_FAILEDupdate_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). Поведение должно стать одним из (выбор — архитектор):

  • экранированный вывод &lt;1м (видится оператору как <1м), либо
  • переформулировка ~0м / < 1 мин с последующим экранированием. Инвариант: для любого входа _fmt_minutes (включая , , ~Nм от _capped_review_str) результат, попав в parse_mode=HTML, не ломает парсер. _fmt_minutes сохраняет never-raise (нечисловой/None вход → ).

FR-2 — HTML-безопасность всех данных карточки (⇒ BR-2)

Каждое подставляемое значение-данные, попадающее в текст render_task_tracker, экранируется html.escape(...) ровно один раз перед вставкой в HTML-текст. Перечень полей-данных:

Поле Источник Текущий статус
Заголовок задачи titleesc_title уже экранирован ✓ (не дублировать)
Длительности стадий / BRD / done _fmt_minutes, _capped_review_str дыра (FR-1)
Статус-лейбл карточки _card_status_labelstatus_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 не рестартуется в рамках разработки.