hermes-android/agy-work/TASK-2026-08-24-07-pairing-protocol.md

129 lines
15 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.

# Task 07: Протокол сопряжения и hermes-pair (hermes-android)
**Repo:** `ochenstarik-ui/hermes-android`
**Assigned to:** Antigravity (режим оркестратора, два кодера)
**Priority:** HIGH (две реализации одного протокола расходятся в четырёх правилах)
**Date:** 2026-08-24
**Base SHA:** результат задания 06 — указать фактический SHA при выдаче
**Зависимость:** задание 06 принято — отпечаток сертификата, закрепляемый при сопряжении, уже существует.
## Роли и протокол
| Роль | Модель | Что делает |
|---|---|---|
| Оркестратор | Antigravity | Разбивает работу, маршрутизирует, принимает результат |
| Кодер 1 | Gemini Flash 3.7 high | Пункты 26 §Scope (реализация по готовой спецификации) |
| Кодер 2 | Gemini Pro high | **Пункт 1 (спецификация)**, затем проверка и доводка работы кодера 1 |
Порядок здесь обратный обычному: спецификация пишется первой и её пишет кодер 2. Кодер 1 не приступает, пока §Scope 1 не зафиксирован письменно и не принят оркестратором — иначе два исполнителя разойдутся так же, как разошлись две существующие реализации.
**Раунд 0 (кодер 2).** Спецификация протокола v1 и набор тест-векторов. Результат — файл в репозитории, а не текст в чате.
**Раунд 1 (кодер 1).** Тесты из §Required tests на векторах спецификации, фиксация падения на base SHA, затем код по пунктам 26.
**Раунд 2 (кодер 2).** Независимое воспроизведение падения, проход §Anti-checklist с явными отметками, доводка, findings по шкале.
**Раунд 3 (оркестратор).** Приёмка при совпадении поведения обеих реализаций на всех векторах — прогоны с обеих сторон приложены.
## Проблема
**1. Два парсера расходятся в правилах (`PAIR-02`).** На один протокол четыре расхождения:
| Правило | Rust (`pairing.rs`) | Kotlin (`HermesPairingParser.kt`) |
|---|---|---|
| Версия UUID `host_id` | строго v4 (`:149`) | любая версия (`:53-60`) |
| Base64 для `data` и `nonce` | URL-safe с паддингом и без, standard (`:215-219,335-339`) | только URL-safe без паддинга |
| Извлечение `data` | `url.query_pairs()` | `split("&").map { split("=") }[1]` (`:24-25`) |
| Форма URI | `hermes://pair` и `hermes:/pair` (`:305`) | только `host == "pair"` |
Kotlin-разбор `data` обрезает значение по первому `=`, то есть ломается на любом паддинге, и не выполняет URL-декодирование. Пустой сегмент в query (`&&`) даёт `IndexOutOfBounds`, замаскированный общим `catch` в «Unknown error». Любой генератор, кроме этой конкретной сборки Rust, будет отвергнут либо разобран неверно.
**2. QR перегенерируется раз в секунду (`PAIR-01`).** `cli.rs:229-259` — цикл на каждой итерации вызывает `create_pairing_payload`: новый nonce, новый `expires_at`. Картинка меняется под камерой, а счётчик «Expires in» тикает по собственному таймеру и сбрасывается независимо от реального TTL.
**3. Nonce не используется (`SEC-09`).** `HermesPairingParser.kt:87-97` проверяет форму nonce и забывает о нём: хосту он не отправляется, доказательства владения нет. QR, снятый со спины в пределах TTL, равноценен оригиналу. `host_id` из payload молча перепривязывает существующую запись хоста — спасает только предупреждение о смене endpoint.
**4. Ручной ввод адреса не валидируется (`SEC-10`).** `HostsViewModel.kt:59-72` кладёт строку в БД как есть; значение без схемы в `convertHttpToWsUrl` (`HermesHostRuntime.kt:265`) превращается в `ws://`.
**5. Ресурсы на каждый опрос (`PAIR-03`).** `app.rs:145-172` поднимает `std::thread` и внутри собирает свежий однопоточный Tokio-runtime, при том что приложение уже под `#[tokio::main]`; `HermesProbeClient::new()` создаётся заново на каждый опрос.
**6. Только IPv4 (`PAIR-04`).** `network.rs:73` обрабатывает лишь `IfAddr::V4`.
**7. Мелочи (`PAIR-05`, `PAIR-06`, `PAIR-07`).** Тихая зона QR 2 модуля в терминале и 3 в egui при требуемых спецификацией 4 (`qr.rs:11,69`) — распознавание с экрана хуже, особенно на тёмном фоне. Таймаут опроса жёстко 2 с без ретраев (`hermes.rs:54`): на загруженной машине хост ошибочно показывается как Offline. Конфиг пишется без ограничения прав, `display_name` нельзя задать из CLI (`config.rs:78`, `cli.rs:21-47`).
## Scope
**1. Спецификация v1 (кодер 2, до всего остального).**
Файл `docs/pairing-protocol-v1.md`: поля payload и их типы, обязательность, допустимые кодировки, форма URI, правила валидации, границы TTL и clock skew, поведение при каждом нарушении.
По каждому из четырёх расхождений — решение и обоснование, а не «как в Rust». В частности: требовать ли UUIDv4 (и что делать с уже сгенерированными `host_id`), какой набор Base64 канонический.
Приложить `docs/pairing-vectors.json` — общие тест-векторы: валидные payload и по одному невалидному на каждое правило. Векторы используются обеими сторонами.
**2. Приведение обеих реализаций к спецификации (`PAIR-02`).**
Kotlin: разбор query через `Uri`/`URLDecoder`, а не `split`; поддержка канонического набора Base64; форма URI по спецификации; отсутствие `IndexOutOfBounds` на любом входе.
Rust: убрать варианты, которые спецификация не разрешает.
Общий `catch`, превращающий любую ошибку в «Unknown error», заменить на различимые причины — иначе следующее расхождение снова окажется невидимым.
**3. Стабильный QR (`PAIR-01`).**
Payload генерируется один раз и перерисовывается; пересоздание — только по истечении TTL. Счётчик считает по `expires_at` самого payload, а не по отдельному таймеру. Проверить оба режима: `--terminal` и GUI.
**4. Nonce как доказательство владения (`SEC-09`).**
Nonce отправляется хосту при первом обращении; повторное использование отвергается. Если контракт хоста этого сейчас не поддерживает — не выдумывать метод: зафиксировать в `OPEN QUESTIONS` требование к стороне хоста и реализовать то, что возможно на клиенте (одноразовость в пределах устройства: использованные nonce запоминаются до истечения TTL).
Перепривязка существующего `host_id` к новому endpoint остаётся возможной только через явное подтверждение — проверить, что диалог показывается во всех ветках, включая совпадающий endpoint с другим отпечатком (задание 06).
**5. Валидация ручного ввода (`SEC-10`).**
Нормализация адреса при сохранении, схема по умолчанию `https://`, отклонение некорректного URL в форме, предупреждение о дубликате по endpoint.
**6. Rust-часть (`PAIR-03`, `PAIR-04`, `PAIR-05`, `PAIR-06`, `PAIR-07`).**
Один `reqwest::Client` и `Handle` существующего runtime вместо потока с новым runtime на каждый опрос.
Поддержка IPv6 с корректным экранированием в URL (`[::1]:9119`) и приоритезацией наравне с IPv4.
Тихая зона 4 модуля в обоих рендерерах, константа в одном месте.
Таймаут опроса настраиваемый, один быстрый повтор перед вердиктом Offline.
Права 0600 на конфиг под Unix; флаги `--display-name` и `--reset-host-id`.
## Do not change
- Транспорт, БД, UI Android сверх пунктов 45.
- Формат токенов и авторизацию — задание 06.
- Схему БД — миграция для хранения использованных nonce версионируется по правилам задания 02.
- Публикацию бинарников — задание 05.
## Anti-checklist
1. Спецификация написана после кода и описывает получившееся поведение. Требование: файл спецификации закоммичен раньше кода — проверить по истории.
2. Векторы покрывают только валидные случаи; на каждое правило нужен и невалидный.
3. Kotlin приведён к спецификации, Rust не тронут — расхождение осталось, просто сместилось. Прогон векторов обязателен с обеих сторон.
4. `IndexOutOfBounds` на `&&` формально не воспроизводится, потому что общий `catch` остался. Проверять надо различимую причину ошибки, а не отсутствие падения.
5. QR стабилизирован в `--terminal`, а GUI по-прежнему перегенерирует. Проверить оба режима.
6. Счётчик «Expires in» считает по локальному таймеру, синхронизированному вручную с TTL, — при смене payload разъедется снова. Требование: источник истины — `expires_at`.
7. Nonce «используется»: сохраняется локально, но проверка одноразовости не переживает перезапуск приложения.
8. Нормализация адреса ломает уже сохранённые хосты в БД — миграция не предусмотрена.
9. IPv6 добавлен в обнаружение интерфейсов, но URL собирается без квадратных скобок.
10. В отчёте `green` для незапущенной команды (`AGENTS.md §3`).
## Definition of Done
- Все векторы из `pairing-vectors.json` дают одинаковый вердикт в Kotlin и в Rust — приложены оба прогона.
- Ни один вход не приводит к необработанному исключению; причина отказа различима.
- QR в обоих режимах стабилен в пределах TTL; счётчик соответствует `expires_at`.
- Повторное использование того же QR отвергается (в пределах реализованного уровня, с явным указанием, что осталось за стороной хоста).
- Ручной ввод без схемы не даёт незашифрованного соединения по умолчанию.
- `hermes-pair`: один клиент и один runtime на процесс; IPv6 работает; тихая зона 4; конфиг с правами 0600; флаги CLI на месте.
- Все существующие тесты зелёные, ни один не удалён и не ослаблен.
## Required tests
Kotlin `core/pairing/PairingVectorsTest.kt` — прогон всех векторов из общего файла; каждый невалидный даёт свою причину. Обязан падать на base SHA (минимум на паддинге и на `&&`).
Kotlin `feature/hosts/HostUrlNormalizationTest.kt` — нормализация и отклонение некорректного ввода.
Rust `tests/vectors.rs` — тот же файл векторов, тот же вердикт.
Rust `tests/qr_stability.rs` — payload не меняется в пределах TTL.
Rust — тесты на IPv6-обнаружение и на сборку URL со скобками.
## Required verification
```text
./gradlew --no-daemon testDebugUnitTest
./gradlew --no-daemon lint
./gradlew --no-daemon assembleDebug
cd hermes-pair && cargo test --all-targets && cargo clippy -- -D warnings
cd hermes-pair && cargo run -- qr --port 9119 # визуальная проверка стабильности QR
```
## Result
`agents/antigravity/done/TASK-2026-08-24-07-pairing-protocol.md` — разделы `## Кодер 2: спецификация`, `## Кодер 1`, `## Кодер 2 (review + доработка)`, `## Вердикт оркестратора`. Отдельно — таблица «правило → решение → обоснование» по четырём расхождениям.