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

133 lines
12 KiB
Markdown
Raw 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.
---
work_item: ORCH-095
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-09
model_used: 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`). Поведение должно стать одним из (выбор — архитектор):
- экранированный вывод `&lt;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` не рестартуется в рамках разработки.