129 lines
15 KiB
Markdown
129 lines
15 KiB
Markdown
# 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 | Пункты 2–6 §Scope (реализация по готовой спецификации) |
|
||
| Кодер 2 | Gemini Pro high | **Пункт 1 (спецификация)**, затем проверка и доводка работы кодера 1 |
|
||
|
||
Порядок здесь обратный обычному: спецификация пишется первой и её пишет кодер 2. Кодер 1 не приступает, пока §Scope 1 не зафиксирован письменно и не принят оркестратором — иначе два исполнителя разойдутся так же, как разошлись две существующие реализации.
|
||
|
||
**Раунд 0 (кодер 2).** Спецификация протокола v1 и набор тест-векторов. Результат — файл в репозитории, а не текст в чате.
|
||
**Раунд 1 (кодер 1).** Тесты из §Required tests на векторах спецификации, фиксация падения на base SHA, затем код по пунктам 2–6.
|
||
**Раунд 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 сверх пунктов 4–5.
|
||
- Формат токенов и авторизацию — задание 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 + доработка)`, `## Вердикт оркестратора`. Отдельно — таблица «правило → решение → обоснование» по четырём расхождениям.
|