133 lines
12 KiB
Markdown
133 lines
12 KiB
Markdown
---
|
||
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`). Поведение должно стать одним из (выбор — архитектор):
|
||
- экранированный вывод `<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` не рестартуется в рамках разработки.
|