docs(review): add independent audit, five review rounds, and remediation task

Full audit of the project (93 findings) plus the review record for rounds 1-5:
P0 release blockers, 65482e8, 42dfe2c, the v0.1.1 candidate verdict, and the
review of the previously unreviewed OAuth/wizard/quota work. Includes the
remediation task with acceptance criteria that 42eddb3 was verified against.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hermes Team 2026-08-20 22:22:57 +07:00
parent 51e5b67921
commit 46a185395c
7 changed files with 940 additions and 0 deletions

View file

@ -0,0 +1,256 @@
# Полный аудит Hermes Hub и бэклог доработок
**Дата аудита:** 2026-08-20
**Проверенная ревизия:** `origin/main` @ `5ccfd48` (github.com/ochenstarik-ui/hermes-hub, private)
**Метод:** статический разбор всех исходников + прогон тестов на чистом checkout `origin/main` в отдельном worktree
**Версия:** 0.1.0 (`pyproject.toml`, `compatibility.json`) против 1.3.0 (`about_view.py`, `gui_server.py`)
**Объём:** ~19 500 строк (Python ~15k, C# 0.8k, HTML 0.85k, PS1/BAT 0.25k, docs/config ~2.5k)
> Первая редакция этого документа делалась по локальному снимку `fdf9ecc` (на 3 коммита старше). Все пункты ниже перепроверены по `origin/main`; раздел 2 фиксирует, что именно уже исправлено в `7609ad8`.
---
## 1. Общая оценка
Ядро маршрутизации (`router_engine`, `health_tracker`, `session_affinity`, адаптеры) спроектировано грамотно: чистые слои, dataclass-модели, потокобезопасность, детерминированные тесты с моками. Коммит `7609ad8` («product stabilization») закрыл заметную часть проблем презентационного слоя — появился кэш сканирования, строгий resolver статусов, идентичность приложения в Windows, предпрогрев экранов.
Тем не менее **продукт остаётся неработоспособным как приложение**:
- нативное приложение не запускается на чистой машине — ни один установщик не ставит зависимости UI;
- failover для основного провайдера (Antigravity) не срабатывает никогда из-за формата ошибок `agy_generate`;
- три пользовательских сценария падают с `AttributeError`/`NameError`: удаление credentials, подключение аккаунта по API-ключу, перераспределение ролей;
- настройки не сохраняются и ни на что не влияют;
- «roadmap»-модули (+819 строк) не подключены ни к одному кодовому пути.
**Фактический прогон тестов на чистом checkout `origin/main`:**
`1 failed, 28 passed, 2 skipped` + 1 ошибка сбора (`test_ui_refinement.py` не импортируется без `customtkinter`).
Отчёт `agents/done/…stabilization…md` заявляет «35/35 PASSED» — это верно только на машине, где `dist/` существует локально и зависимости доустановлены руками.
**Зрелость:** ядро — Beta, оболочка — Alpha, упаковка — не готова к дистрибуции.
---
## 2. Что уже исправлено в `7609ad8` (не требует работы)
| Было в аудите | Статус в `origin/main` |
|---|---|
| `TeamView.update_data(app_state)` требует аргумент → главный экран не обновляется | **Исправлено** — сигнатура унифицирована (`app_state=None`) во всех view |
| `scan_all()` вызывается 910 раз за один refresh | **Исправлено** — TTL-кэш 30 с (`unified_health.py:248`) |
| Ложные `HEALTHY`/`QUOTA_EXHAUSTED` у неподключённых слотов | **Исправлено** — введён `STATUS_NOT_CONFIGURED`, устаревшие записи квот чистятся |
| Иконка Python в панели задач | **Исправлено**`SetCurrentProcessExplicitAppUserModelID("HermesHub.Desktop")` + multi-res `.ico` |
| Лаг переключения вкладок | **Исправлено** — предпрогрев всех 8 экранов в `__init__`, переключение через `pack_forget()` |
| Установщик не копирует `assets/branding` | **Исправлено** в новом `installer/HermesHubSetup.py``.cs` и `install.ps1` — нет) |
| Нет документации по разработке/безопасности/производительности | **Исправлено** — добавлены `docs/AUTH.md`, `DEVELOPMENT.md`, `PERFORMANCE.md`, `ROUTER.md`, `SECURITY.md` |
| Нет адаптивности окна | **Исправлено** — debounce-обработчик resize |
---
## 3. P0 — Блокеры (перепроверено на `origin/main`, всё воспроизводится)
| # | Дефект | Файл | Последствие |
|---|---|---|---|
| 1 | `customtkinter` и `pillow` не устанавливаются ни `HermesHubSetup.py`, ни `HermesHubSetup.cs`, ни `install.ps1` | все три установщика | Запуск через `pythonw.exe` завершается молча: ни окна, ни ошибки. Проверено: в `%LOCALAPPDATA%\hermes\hermes-agent\venv` пакета нет |
| 2 | `ProfileAuthManager.get_profile_dir` не существует — это модульная функция, а не метод класса | `profile_manager.py:79` vs `gui_server.py:216`, `hermes_hub_app.py:120`, `add_account_wizard.py:200` | `AttributeError` при удалении credentials и при сохранении API-ключа |
| 3 | `json` не импортирован, но используется | `ui/add_account_wizard.py:200` | `NameError`**Codex и OpenCode Go нельзя подключить через UI вообще** |
| 4 | `AutoAssigner.auto_assign_all` не существует | `hermes_hub_app.py:411`, `team_view.py:188,243` | Кнопки «Перераспределить роли» и «⋮» падают |
| 5 | `agy_generate` возвращает ошибки как успешный `chat.completion`: текст ошибки в `content`, ключа `error` нет | `agy_subprocess.py:584` + `router_engine.py` | `RouterEngine` проверяет `if "error" in response` → не срабатывает → `mark_success`, **failover для Antigravity не работает никогда**, пользователь получает текст ошибки вместо ответа модели |
| 6 | Мастер ждёт `session.status == "error"`, сессия ставит `"failed"` | `add_account_wizard.py:243` vs `profile_oauth.py` | При ошибке OAuth мастер молча висит 120 с |
| 7 | Кнопка «Назначить» шлёт action `assign_role`, обработчика нет | `accounts_view.py:244` vs `hermes_hub_app._handle_action` | Кнопка не делает ничего, без обратной связи |
| 8 | Шаг 4 мастера не применяет выбранную роль — только пишет в журнал | `add_account_wizard.py:_finish` | Выбор пользователя игнорируется |
| 9 | Шаг 3 показывает «✓ Аккаунт успешно проверен» без всякой проверки для API-ключей; список моделей захардкожен (`gpt-4.1`, `qwen-coder`, `deepseek-v2`) | `add_account_wizard.py` | Ложноположительная валидация, вымышленные модели |
---
## 4. P1 — Производительность и отзывчивость UI
| # | Проблема |
|---|---|
| 10 | `_restore_status()` вызывает `get_system_readiness()` **в UI-потоке** через `after(6000, …)`. При холодном/истёкшем кэше — блокировка окна на время сетевого скана 16 профилей |
| 11 | При промахе кэша `scan_all()` опрашивает 16 профилей последовательно (таймаут 10 с на профиль) — нет параллелизма и общего бюджета времени |
| 12 | 5 экранов (`accounts`, `health`, `providers`, `routing`, `logs`) полностью уничтожают и пересоздают виджеты через `winfo_children()` + `destroy()` — мерцание, потеря позиции скролла |
| 13 | Нет периодического автообновления: данные устаревают до ручного нажатия «Обновить» (таймер только для resize) |
| 14 | Тост затирает строку состояния и восстанавливается блокирующим вызовом |
---
## 5. P1 — Логика роутера и корректность состояний
Файлы `router_engine.py`, `health_tracker.py`, `agy_subprocess.py`, `profile_manager.py`, `router_config.py`, `session_affinity.py`, `hermes_plugin.py` в `7609ad8` **не менялись** — все находки в силе.
| # | Проблема | Файл |
|---|---|---|
| 15 | `_CM_LOCK` удерживается всё время subprocess `agy` (до 180+ с) → все Antigravity-профили сериализуются, параллельная работа команды невозможна | `antigravity_adapter.py` |
| 16 | Профильные токены пишутся в **глобальную** запись `gemini:antigravity` Credential Manager и не восстанавливаются → «основной аккаунт Hermes» меняется как побочный эффект маршрутизации; гонки с самим `agy` и Hermes CLI | `antigravity_adapter.py` |
| 17 | `overall_state` не сбрасывается при истечении cooldown (только в `mark_success`) → профиль остаётся «Квота исчерпана» бессрочно | `health_tracker.py:is_healthy` |
| 18 | Роутер решает по состоянию семейства моделей, UI — по `overall_state`: роутер шлёт запросы в профиль, который UI показывает мёртвым | `health_tracker.py` / `unified_health.py` |
| 19 | Session affinity без TTL и вытеснения — неограниченный рост памяти; `session_affinity_ttl_seconds` из шаблона конфига не реализован | `session_affinity.py` |
| 20 | `router_state.json` пишется без межпроцессной блокировки — GUI и процесс Hermes затирают состояние друг друга | `health_tracker.py` |
| 21 | `ProfileHealthRecord.active_leases` всегда 0, но выводится в CLI и API | `health_tracker.py`, `cli_commands.py` |
| 22 | Категория `TRANSIENT` классифицируется, но обрабатывается как обычный отказ — без повтора и задержки | `router_engine.py` |
| 23 | Классификация ошибок по подстрокам (`"429" in err_lower`) — ложные срабатывания на любом тексте с «429» | все адаптеры |
| 24 | `_safe_env()` не используется: `custom_env = dict(os.environ)` передаёт в subprocess `agy` все ключи (`OPENAI_API_KEY` и пр.) | `agy_subprocess.py`, `antigravity_adapter.py` |
| 25 | Нет стриминга ответов — Hermes получает только полный ответ | `router_engine.py` |
| 26 | Нет учёта токенов, стоимости и latency между вызовами | — |
| 27 | В шаблоне `router_profiles.example.yaml` блок `router:` полностью игнорируется загрузчиком (ожидаются ключи верхнего уровня); `max_failover_attempts`, `cooldown_base_seconds`, `cooldown_max_seconds`, `session_affinity_ttl_seconds` отсутствуют в схеме | `router_config.py:load_router_config` |
| 28 | `save_router_config` пишет другую структуру, чем шаблон → первое сохранение («назначить оркестратором») уничтожает комментарии и структуру пользовательского YAML | `router_config.py` |
| 29 | `max_concurrency=1` для Codex и OpenCode во встроенном дефолте, хотя это stateless REST (в шаблоне 2 и 3) — искусственное ограничение пропускной способности | `router_config.py` |
| 30 | `antigravity_llm_execution` перехватывает **все** вызовы `llm_execution` независимо от провайдера и никогда не вызывает `next_call`; ошибка роутера логируется на уровне `debug` | `hermes_plugin.py` |
| 31 | Каталог моделей захардкожен (`gemini-3.7-flash`, `qwen3.8-max`, `kimi-k2.7-code`…) и не сверяется с реальным | `router_config.py` |
| 32 | `discover_models()` определяет каталог, вызывая `agy` с заведомо невалидной моделью и парся текст ошибки | `agy_subprocess.py` |
| 33 | `AutoAssigner.check_duplicate_identity` сравнивает `auth_data["email"]`, но `profile_oauth` сохраняет только `{"token", "auth_method"}`**детектор дубликатов не срабатывает никогда** | `auto_assigner.py` |
---
## 6. P1 — Безопасность
| # | Проблема |
|---|---|
| 34 | `CLIENT_ID`/`CLIENT_SECRET` Google зашиты в код и **склеены из фрагментов строк** (`"GOC" + "SPX" + …`, `oauth.py:26-28`) — приём обхода сканеров секретов, чужой OAuth-клиент. Требует явного решения: собственный клиент или документированное заимствование. Отчёты «0 секретов при сканировании» проходят именно из-за склейки |
| 35 | `ProfileAuthManager.save_profile_auth` пишет токены plaintext без ограничения ACL. `docs/SECURITY_MODEL.md` заявляет изоляцию «правами текущего пользователя ОС», но код прав не выставляет (`chmod` есть только в `CredentialStore` и на Windows не работает) |
| 36 | `access_token` передаётся в query-строке при верификации (`tokeninfo?access_token=…`) — попадает в логи |
| 37 | `CORSMiddleware(allow_origins=["*"], allow_credentials=True)` на локальном API с эндпоинтами `delete`, `set-main`, `oauth/start` (`gui_server.py`) |
| 38 | Лаунчер генерирует и исполняет Python-скрипт `%LOCALAPPDATA%\hermes\hermes_hub_entry.py` при каждом запуске — вектор подмены между записью и стартом |
| 39 | Нет подписи установщика и лаунчера; `dist/checksums.txt` формируется вручную и не проверяется при установке |
| 40 | В `hermes-hub.log` пишутся тексты ошибок провайдеров без санитизации (могут содержать фрагменты токенов) |
---
## 7. P1 — Тесты и качество
| # | Проблема |
|---|---|
| 41 | `pytest tests/` **падает на сборе**: `test_ui_refinement.py``hermes_hub_app``customtkinter` |
| 42 | `test_setup_exe_exists` **падает на чистом checkout**: `dist/` в `.gitignore`, ещё 2 теста уходят в skip. Реальный результат — `1 failed, 28 passed, 2 skipped`, а не заявленные 35/35 |
| 43 | Тесты мутируют реальные пользовательские данные: `do_set_main` пишет `router_active_profile.json`, `do_set_orchestrator` перезаписывает `router_profiles.yaml`, `get_profile_env_dir` создаёт каталоги профилей. Нет фикстуры с временным `HERMES_HOME` |
| 44 | `test_installer.py` **запускает реальный установщик**: копирование в `%LOCALAPPDATA%`, ярлык, запись в реестр |
| 45 | Тесты ходят в сеть (Google tokeninfo, OpenCode `/models`) — недетерминированность |
| 46 | Нет тестов на: `classify_error` всех адаптеров, мастер подключения, YAML round-trip `load↔save`, CLI, логику установщиков |
| 47 | Нет CI (`.github/workflows` отсутствует, репозиторий приватный) |
| 48 | `ruff` в dev-зависимостях, но нет секции `[tool.ruff]` и запуска; нет `mypy` |
---
## 8. P1 — Упаковка, установка, дистрибуция
| # | Проблема |
|---|---|
| 49 | **Два параллельных установщика**: `installer/HermesHubSetup.cs` (711 строк, GUI-мастер, silent-режим, реестр, коды 0/10/11/12) и `installer/HermesHubSetup.py` (101 строка, копирует assets, но без реестра, ярлыка, silent-кодов). Неясно, какой канонический; `dist/HermesHubSetup.exe` собран из `.cs` |
| 50 | Проверка совместимости фиктивна: `compatibility.json` не читается, `MIN_HERMES_VERSION` ни с чем не сравнивается, `IsHermesCompatible = true` всегда при наличии Python → **код возврата 11 недостижим**. То же в `install.ps1` (версия печатается, но не проверяется) |
| 51 | `installer/HermesHubSetup.cs` и `scripts/install.ps1` не копируют `assets/branding` (исправлено только в `.py`-варианте) |
| 52 | `ui/assets.py:39` — захардкоженный fallback `Path("E:/Agent projects/hermes-hub")` в исходниках |
| 53 | `build_launcher.ps1` не передаёт `/win32icon:HermesHub.ico`у самого `.exe` нет иконки (иконка ставится только в рантайме через `iconbitmap`) |
| 54 | `install.ps1` копирует `__pycache__` |
| 55 | `launcher/HermesHub.exe` — собранный бинарник в git; собранный установщик, наоборот, не отслеживается |
| 56 | Нет single-instance guard — повторный клик по ярлыку открывает второе окно |
| 57 | Нет логирования запуска: при `pythonw.exe` любая ошибка импорта = полная тишина |
| 58 | Нет механизма обновления в самом приложении |
| 59 | Версия расходится: `0.1.0` (pyproject, compatibility.json, installer) против `1.3.0` (About, FastAPI) |
| 60 | `scripts/launch_hermes_hub.bat` запускает несуществующий веб-режим на `:8765`; CLI-аргументы `hub --port/--no-browser` парсятся и игнорируются |
---
## 9. P2 — Мёртвый и неподключённый код
| # | Объект | Строк | Статус |
|---|---|---|---|
| 61 | `supervisor/lifecycle_supervisor.py`, `supervisor/policies.py`, `capability/capability_matrix.py`, `skills/skill_registry.py`, `scheduler/task_safety.py`, `adapters/deepseek_adapter.py` | 819 | **Roadmap-модули из `7609ad8` не подключены ни к одному кодовому пути.** Импортируются только тестами (`task_safety` и `deepseek_adapter` — вообще ничем). `DeepSeekResponsesAdapter` не зарегистрирован в `_ADAPTERS` → недостижим через `get_adapter()` |
| 62 | `gui_server.py` + `gui_cockpit.html` | 1177 | Параллельный веб-стек, не запускается ниоткуда; `fastapi`/`uvicorn` остаются обязательными зависимостями |
| 63 | `runtime.generate_chat_completion`, `antigravity_client.py`, `transform.py`, `openai_compat.py`, `cloudcode.py` | ~590 | Прямой API-путь к Antigravity, не используется (весь трафик через `agy` CLI) |
| 64 | `ui/views/dashboard_view.py` («Главная») | 141 | Не подключён к навигации |
| 65 | `ui/splash.py` (`SplashScreen`) | 77 | Не используется |
| 66 | `HealthTracker.get_status_summary()` | — | Не вызывается ниоткуда |
| 67 | Резолвинг `hermes_home` | — | Продублирован в 5 файлах с тремя разными реализациями (`HERMES_HOME` / `LOCALAPPDATA` / `~/.hermes`) |
| 68 | `do_set_main`/`do_test_profile`/`do_delete_credentials` | — | Дублируются между `gui_server.py` и `hermes_hub_app.py` |
---
## 10. P2 — Документация
| # | Проблема |
|---|---|
| 69 | `docs/ARCHITECTURE.md` и `CHANGELOG.md` описывают «Edge App Mode», «FastAPI Backend :8765», «health check gate (HTTP 200)» — этой архитектуры больше нет |
| 70 | `agents/AGENTS.md` и отчёты в `agents/done/` указывают Source of Truth `E:\Agent projects\hermes-hub`; ссылки вида `file:///E:/Agent%20projects/...` мертвы |
| 71 | `docs/SECURITY_MODEL.md` не описывает фактическую подмену глобальной записи `gemini:antigravity` при маршрутизации |
| 72 | Нет описания схемы `router_profiles.yaml` (единственный источник истины — код) и справочника CLI |
| 73 | Отчёты в `agents/done/` содержат утверждения, не подтверждаемые кодом: «35/35 PASSED», «интерактивные переключатели параметров», «9/9 экранов», «иконка вшита в лаунчер». Требуется выравнивание с фактами |
| 74 | UI жёстко на русском, CLI на английском, строки не вынесены — i18n невозможен |
---
## 11. P2P3 — Функциональные пробелы
| # | Пробел |
|---|---|
| 75 | Настройки не сохраняются: `CTkSwitch`/`CTkOptionMenu` не привязаны к `self.settings` (нет ни одного `command=`), `_save_settings()` пишет неизменённый словарь |
| 76 | Ни один параметр не влияет на роутер: `failover_attempts`, `session_affinity`, `auto_failover`, `model_timeout_sec`, `monitoring_interval_min` нигде не читаются |
| 77 | Пути в «Настройках» указывают на `hermes\router_profiles.yaml` и `hermes\auth.json`; реальные — `hermes\config\router_profiles.yaml` и `hermes\agy_profiles\<id>\auth.json` |
| 78 | Нет ручного назначения профиля на роль (только автоматическое) |
| 79 | Экран «Маршрутизация» read-only: нельзя изменить цепочку failover |
| 80 | Нельзя добавить или удалить слот — 16 профилей жёстко зашиты в коде |
| 81 | Кнопка «Обновить модели» не вызывает `adapter.discover_models()` — показываются `preferred_models` из конфига |
| 82 | Фоновый мониторинг здоровья отсутствует, хотя переключатель для него есть |
| 83 | Нет уведомлений (tray/toast) об исчерпании квоты и переключении на резерв |
| 84 | Нет экспорта/импорта конфигурации и резервного копирования профилей |
| 85 | Нет истории квот и использования |
| 86 | Журнал живёт в памяти (200 записей) и обнуляется при перезапуске; `hermes-hub.log` пишется, но не читается обратно |
| 87 | В журнале нет поиска и фильтра по уровню (только по категории) |
| 88 | Тема жёстко тёмная, нет клавиатурной навигации и доступности |
| 89 | `compatibility.json` заявляет `linux`/`darwin`, но Credential Manager, пути и лаунчер — только Windows |
---
## 12. Рекомендуемый план работ
### Этап 1 — Вернуть работоспособность (P0, ~2 дня)
1. Устанавливать `customtkinter`/`pillow` в venv Hermes из установщика; проверять импорт после установки и падать с внятной ошибкой.
2. Исправить `get_profile_dir` (сделать `@staticmethod` либо импортировать функцию), добавить `import json` в мастер.
3. Реализовать `AutoAssigner.auto_assign_all()` либо убрать кнопки.
4. Ввести маркер ошибки в `agy_generate` (`{"error": …}` или исключение) и восстановить failover для Antigravity; покрыть регрессионным тестом.
5. Починить мастер: статус `"failed"`, реальная валидация API-ключа, реальные модели, применение выбранной роли, обработчик `assign_role`.
6. Прогонять `pytest` в изолированном `HERMES_HOME`; убрать запуск установщика и сетевые вызовы из тестов; сделать `test_setup_exe_exists` skip-ом при отсутствии `dist/`.
### Этап 2 — Корректность состояний и отзывчивость (P1, ~3 дня)
7. Все сетевые операции — только в фоне; `after(0, …)` получает готовые данные (включая `_restore_status`).
8. Параллельный опрос профилей с общим бюджетом времени вместо последовательного.
9. Обновление виджетов in-place вместо `destroy()`/пересоздания в 5 экранах.
10. Сброс `overall_state` при истечении cooldown; единый источник истины по здоровью для роутера и UI.
11. TTL и вытеснение в `SessionAffinityTracker`; файловая блокировка `router_state.json`.
12. Сузить `_CM_LOCK` до операции подмены credential и восстанавливать прежнюю запись — либо перейти на изоляцию только через `USERPROFILE`.
13. Связать `settings_view` с реальными параметрами роутера; выровнять пути.
14. Починить `check_duplicate_identity` (сохранять `email` в `auth.json` при OAuth).
### Этап 3 — Безопасность и упаковка (P1, ~2 дня)
15. Решение по OAuth-клиенту; прекратить склейку секрета из фрагментов.
16. ACL/DPAPI для `auth.json`; санитизация логов; убрать токен из query-строки.
17. Убрать `allow_origins=["*"]` либо удалить веб-стек целиком.
18. Выбрать один установщик; реальная проверка версии из `compatibility.json` (сделать код 11 достижимым); исключить `__pycache__`; убрать `E:/…` fallback; встроить иконку через `/win32icon`.
19. Логирование запуска в файл до инициализации UI; single-instance guard.
### Этап 4 — Долг и документация (P2, ~2 дня)
20. Подключить roadmap-модули к реальным кодовым путям (как минимум зарегистрировать `DeepSeekResponsesAdapter` в `_ADAPTERS`) либо явно пометить их как экспериментальные.
21. Удалить или вынести в `legacy/` веб-стек и прямой API-путь; убрать `fastapi`/`uvicorn` из обязательных зависимостей.
22. Единый модуль `paths.py`; устранить дублирование `do_*`.
23. Синхронизировать версию (0.1.0), обновить `ARCHITECTURE.md`/`CHANGELOG.md` под нативную архитектуру, исправить пути `E:\`.
24. Выровнять отчёты в `agents/done/` с фактическим состоянием.
25. Добавить CI (pytest + ruff) и конфигурацию ruff.
### Этап 5 — Развитие (P3)
26. Ручное назначение ролей и редактор цепочек failover.
27. Динамические слоты, реальный `discover_models`.
28. Фоновый мониторинг + уведомления о квотах.
29. Персистентный журнал с поиском; статистика использования.
30. Стриминг ответов, учёт токенов и стоимости.
31. Экспорт/импорт конфигурации, бэкап профилей.
---
## 13. Сводка
| Приоритет | Пунктов | Область |
|---|---|---|
| **Исправлено в `7609ad8`** | 8 | Кэш сканирования, resolver статусов, идентичность Windows, предпрогрев, assets, docs |
| **P0** — блокеры | 9 | Запуск приложения, failover Antigravity, падения UI, мастер подключения |
| **P1** — критично | 51 | Отзывчивость, корректность состояний, безопасность, тесты, упаковка |
| **P2** — важно | 20 | Неподключённый код, архитектурный долг, документация |
| **P3** — развитие | 9 | Новая функциональность |
| **Итого к работе** | **89** | |

View file

@ -0,0 +1,150 @@
# Ревью: `2a97e80` + `7926de9` (закрытие P0 и релизный конвейер)
**Ревьюер:** Claude (роль «Ревьюер»)
**База:** `5ccfd48`**проверено:** `7926de9`
**Метод:** построчный разбор диффа + исполняемая проверка каждого утверждения в изолированном `HERMES_HOME`
**Вердикт: ⚠️ Принять нельзя — 4 блокирующих замечания.** Из 9 P0 фактически закрыто 8; один закрыт с регрессией. Дополнительно найдено 4 дефекта в новом коде.
---
## 1. Подтверждено закрытым (проверено исполнением)
| P0 | Проверка | Итог |
|---|---|---|
| №2 `get_profile_dir` | `hasattr(ProfileAuthManager,'get_profile_dir')``True`, поддержаны обе сигнатуры | ✅ |
| №3 `json` в мастере | импорт присутствует, `_save_key` отрабатывает | ✅ |
| №4 `auto_assign_all` | метод существует, выполняется | ✅ (см. B2) |
| №6 статусы OAuth | `("error","failed","cancelled")` + `"timeout"``add_account_wizard.py:265` | ✅ |
| №7 `assign_role` | обработчик + модалка `hermes_hub_app.py:408,419` | ✅ |
| №8 роль из шага 4 | вызывается `assign_profile_to_role` | ✅ (см. B2) |
| №9 валидация ключа | реальные `verify_codex_token` / `verify_opencode_token`, честная метка «НЕ ПРОВЕРЕН» | ✅ |
| №1 зависимости UI | `HermesHubSetup.py:52` ставит пакеты в venv Hermes + проверка импорта. Пины `customtkinter>=6.0.0`, `pillow>=12.3.0` сверены с PyPI — версии существуют | ✅ |
**Сверх P0 — зачтено:** единая версия `0.1.1` (`version.py` → pyproject → compatibility.json → About); `paths.py` вместо пяти реализаций резолвинга и захардкоженного `E:/Agent projects`; сброс `overall_state` по истечении cooldown (проверено: `overall=healthy` при `family=quota-exhausted`, после истечения — оба healthy); типизированные исключения вместо угадывания по подстрокам на уровне роутера; `conftest.py` с изоляцией `HERMES_HOME` (проверено — работает); маркеры pytest и `addopts` с отключением `installer/live/network`; CI и `release_gate.py`.
**Тесты:** `41 passed, 1 failed, 3 deselected`. Единственный failed — `test_p0_1_installer_dependencies`, потому что в venv Hermes на этой машине `customtkinter` ещё не установлен (установщик не запускался). Регрессионный тест на failover `test_p0_5_antigravity_failover_on_quota` присутствует и проходит.
---
## 2. Блокирующие замечания
### B1. Регрессия: не-роутерный путь падает с `IndexError`
`agy_subprocess.py:584` теперь возвращает `{"error": {...}}` без `choices`. Для роутера это правильно, но `_error_completion` обслуживает **два** пути, а `hermes_plugin.py` не менялся.
Путь 2 (`hermes_plugin.antigravity_llm_execution`, когда роутер отключён `config.enabled=false` или выбросил исключение):
```
completion = agy_generate(request) # -> {"error": {...}}
return openai_completion_object(completion) # -> choices = []
```
Проверено исполнением:
```
после openai_completion_object: choices = []
обращение Hermes к choices[0]: IndexError: list index out of range
```
Было: пользователь видел текст ошибки как ответ ассистента. Стало: падение транспорта Hermes.
**Требуется:** обработать `{"error": …}` в `antigravity_llm_execution` — либо конвертировать в completion с текстом ошибки, либо пробрасывать типизированное исключение. Тест на путь с `enabled=false` обязателен.
---
### B2. `assign_profile_to_role` создаёт фиктивные роли и не меняет реальные
`auto_assigner.py:assign_profile_to_role` вызывает `config.get_role_policy(role_name)`, а тот для неизвестной роли возвращает **generic-политику с `preferred_chain = list(self.profiles.keys())`** — все 16 профилей. Результат сохраняется в конфиг.
При этом `auto_assign_all` раздаёт роли из списка `["orchestrator","coder","reviewer","researcher","tester","general"]`, а мастер — `coder/researcher/general/spare`. Реальные роли роутера: `orchestrator`, `coder-primary`, `coder-secondary`, `reviewer`, `research`, `fast`. Совпадают только `orchestrator` и `reviewer`.
Проверено в изолированном окружении:
```
роли ДО : [coder-primary, coder-secondary, fast, orchestrator, research, reviewer]
роли ПОСЛЕ: [coder, coder-primary, coder-secondary, fast, orchestrator, research, reviewer]
длина цепочки роли coder : 16
цепочка coder : [ag-w1, codex-orch, ..., ag-cold-1, ag-cold-2, ag-cold-3, opengo-1, ...]
цепочка coder-primary : [codex-worker-1, ag-w1, opengo-3] <- НЕ ИЗМЕНИЛАСЬ
```
Итог: назначение роли не влияет на маршрутизацию, а конфиг засоряется мусорными ролями с цепочками из 16 профилей, включая отключённые холодные резервы.
`test_p0_4` и `test_p0_7` этого не ловят, потому что проверяют механику («выполнилось без `AttributeError`», «цепочка изменилась и сохранилась»), а не семантику — что имя роли маршрутизируемое.
**Требуется:** маппинг человекочитаемых имён на реальные (`coder` → `coder-primary`, `researcher``research`, `tester`/`general` → `fast` или отдельная роль), отказ при неизвестной роли, и запрет на persist generic-политики со всеми профилями. В тесты — проверка, что `role_name in config.roles` **до** назначения.
---
### B3. Каждое назначение роли уничтожает пользовательский YAML
`save_router_config` пишет схему, отличную от шаблона. Раньше это срабатывало редко, теперь — при **каждом** подключении аккаунта через мастер и при каждом `auto_assign_all`.
Проверено на копии `config/router_profiles.example.yaml`:
```
шаблон: 5 строк комментариев, блок router: 1
после одного назначения роли: 0 строк комментариев, блок router: 0
```
Блок `router:` (`max_failover_attempts`, `cooldown_base_seconds`, `cooldown_max_seconds`, `session_affinity_ttl_seconds`) загрузчиком и так игнорируется — но пользователь, отредактировавший его, молча теряет правки.
**Требуется:** либо привести `load/save` к одной схеме и покрыть тестом round-trip (`load → save → load` без потерь), либо перейти на редактирование с сохранением структуры.
---
### B4. Rate limit классифицируется как исчерпание квоты — 30 минут вместо 60 секунд
`antigravity_adapter.py:invoke` — первая ветка ловит `("quota","resource_exhausted","429","limit","exhausted")`, ветка `"rate"` идёт **после** и для реального 429 недостижима: сообщение «429 Too Many Requests: rate limit exceeded» содержит и `429`, и `limit`.
Проверено исполнением:
```
вход : Antigravity error: agy error: 429 Too Many Requests: rate limit exceeded
итог : QuotaExceededError -> category=quota-exhausted cooldown=1800s
```
Профиль паркуется на 30 минут вместо 60 секунд. При цепочке из трёх профилей серия временных 429 выводит роль в «Failover Exhausted».
**Требуется:** проверять `rate`/`too many requests`/`429` **до** квотной ветки, либо различать по коду ответа, а не по подстроке.
---
## 3. Существенные замечания (не блокируют, но лучше в этом же заходе)
**S1. Потеряно время сброса квоты.** Типизированные исключения обрабатываются в начале `classify_error` и используют `exc.reset_in_sec or 1800`, но `reset_in_sec` при выбросе в `invoke` не заполняется. Regex-разбор («resets in 2h» → 7200 s) остался ниже и для типизированных исключений недостижим. Проверено: «individual quota reached, resets in 2h» → `cooldown=1800s`. Раньше было 7200. Прокидывайте распарсенную длительность в конструктор исключения.
**S2. Встроенный апдейтер нерабочий.** `update_manager.py:82` тянет манифест с `raw.githubusercontent.com/ochenstarik-ui/hermes-hub/main/dist/update_manifest.json`. Файл не отслеживается git (`dist/` в `.gitignore`), репозиторий приватный. Проверено: `HTTP 404`. Проверка обновлений всегда будет падать. Нужен публичный канал (GitHub Releases API) или отдельная ветка/репозиторий манифестов.
**S3. Апдейтер проверяет целостность, но не подлинность.** SHA-256 берётся из того же манифеста, что и `package_url`, — при подмене манифеста хеш подменяется вместе с пакетом. Плюс `HERMES_HUB_UPDATE_URL` переопределяется переменной окружения, а `package_url` скачивается без allowlist хостов. Для механизма, который распаковывает код в рабочий каталог и затем исполняется, нужна подпись пакета либо жёсткая привязка к домену релизов. (`extractall` от Zip Slip защищён — CPython нормализует пути, здесь претензий нет.)
**S4. `release_gate.check_security_zero_secrets` даёт ложное «0 секретов».** Проверка ищет отслеживаемые **файлы**-секреты и не видит `CLIENT_SECRET` в `oauth.py:28`, склеенный из фрагментов строк специально для обхода сканеров. Гейт будет зелёным при наличии секрета в коде.
**S5. `py_compile` по `dest.rglob("*.py")`** (`update_manager.py:194`) пройдёт по `.venv`, если апдейт применяется к каталогу разработки, — это тысячи файлов и вероятные ложные сбои с откатом. Ограничьте обход каталогом `src`.
**S6. Настройки по-прежнему не сохраняются.** Ни один `CTkSwitch`/`CTkOptionMenu` в `settings_view.py` (строки 78, 87, 96, 116, 124) не имеет `command=`/`variable=`, `_save_settings()` пишет неизменённый словарь дефолтов. Кнопка «💾 Сохранить» ничего не сохраняет. В отчёте это заявлено как «интерактивные переключатели» — расхождение.
**S7. `test_installer` всё ещё меняет систему.** Песочница через `LOCALAPPDATA`/`HERMES_HOME` в env закрывает файловое дерево, но `CreateStartMenuShortcut` пишет в `%APPDATA%\...\Start Menu`, а `RegisterInWindowsUninstall` — в `HKCU`. Ни то, ни другое не перенаправляется. Смягчено маркером `installer` и `addopts`, но при `-m installer` система снова будет изменена.
**S8. Отсутствие UI-зависимости роняет весь прогон.** `test_ui_refinement.py:11` импортирует `hermes_hub_app` на уровне модуля → `ModuleNotFoundError: customtkinter` прерывает сбор всей сессии («Interrupted: 1 error during collection»), а не пропускает один модуль. Нужен `pytest.importorskip("customtkinter")`.
**S9. Мастер не останавливает опрос при закрытии окна.** `_polling_active` сбрасывается только при переходе на шаг 3 (`add_account_wizard.py:279`). Закрытие модалки во время OAuth оставляет поток жить до 120 с, и он вызовет `self.after(0, …)` на уничтоженном виджете. Нужен `destroy()`/`WM_DELETE_WINDOW` со сбросом флага.
---
## 4. Осталось открытым из аудита (вне scope этого коммита)
Не претензия к коммиту — фиксирую, что эти пункты не затронуты:
- `_CM_LOCK` по-прежнему удерживается на всё время subprocess `agy` (`antigravity_adapter.py:invoke`) → профили Antigravity сериализуются.
- Глобальная запись `gemini:antigravity` перезаписывается без восстановления → «основной аккаунт Hermes» меняется как побочный эффект маршрутизации.
- Session affinity без TTL; `router_state.json` без межпроцессной блокировки.
- `hermes_plugin` перехватывает все вызовы `llm_execution` и не вызывает `next_call`.
- 5 экранов пересоздают виджеты через `destroy()`; нет периодического автообновления.
- Roadmap-модули (`supervisor`, `capability`, `skills`, `scheduler`, `deepseek_adapter`) по-прежнему не подключены; `DeepSeekResponsesAdapter` не зарегистрирован в `_ADAPTERS`.
- Веб-стек `gui_server.py` + `gui_cockpit.html` и прямой API-путь остаются мёртвыми; `fastapi`/`uvicorn` — в обязательных зависимостях.
---
## 5. Резюме для исполнителя
Порядок правок: **B1** (регрессия, ломает работающий сценарий) → **B4** (однострочная перестановка веток) → **B2** + **B3** (связаны: маппинг ролей и сохранение конфига) → S1, S8, S9 (дёшево) → S2S5 (апдейтер, до объявления релиза).
Релизным гейтом это пропускать нельзя: `check_p0_release_gate` рапортует «9/9 P0 verified», хотя P0-5 закрыт с регрессией, а P0-4/P0-7 проходят на фиктивных ролях.

View file

@ -0,0 +1,104 @@
# Ревью, раунд 2: `65482e8` (ответ на замечания B1B4, S1S9)
**База:** `7926de9`**проверено:** `65482e8`
**Метод:** повтор тех же исполняемых проверок, что выявили дефекты в раунде 1
**Вердикт: ✅ Блокеры закрыты, коммит принимается в `main`.**
**⚠️ Релиз v0.1.1 объявлять рано** — канал обновлений не работает, а релизный гейт даёт ложно-зелёный результат по секретам.
---
## 1. Блокеры — все 4 закрыты (проверено повтором)
**B1 — не-роутерный путь.** Защита в двух местах: guard в `hermes_plugin.antigravity_llm_execution` (обе ветки) и в самом `openai_completion_object`. Повтор моей проверки:
```
choices: 1
choices[0].message.content: Antigravity error: ... individual quota reached
```
Раньше — `IndexError`. Закрыто.
**B2 — фиктивные роли.** Введён `CANONICAL_ROLE_MAP`, неизвестные роли отклоняются:
```
coder -> ok=True назначен на роль 'coder-primary'
researcher -> ok=True назначен на роль 'research'
tester -> ok=True назначен на роль 'fast'
general -> ok=True назначен на роль 'fast'
spare -> ok=False Неизвестная роль маршрутизатора: 'spare'
роли ПОСЛЕ: без изменений (мусорные роли не создаются)
цепочка coder-primary: ['ag-w1', 'codex-worker-1', 'opengo-3'] <- реально обновилась
макс. длина цепочки: 4 (было 16)
```
Закрыто.
**B3 — разрушение YAML.** Схема `load`/`save` сведена. Блок `router:` сохраняется целиком со всеми значениями (`enabled`, `default_role`, `max_failover_attempts`, `cooldown_base_seconds`, `cooldown_max_seconds`, `session_affinity_ttl_seconds`); профилей 16/16, ролей 6/6, cooldown'ы не поплыли. Закрыто по существу — см. остаточное R3.
**B4 — 429 как квота.** Порядок веток исправлен:
```
429 Too Many Requests: rate limit exceeded -> RateLimitedError rate-limited retry_delay=60s
individual quota reached, resets in 2h -> QuotaExceededError quota-exhausted cooldown=7200s
```
Закрыто (заодно S1 — длительность сброса снова парсится: 7200 вместо 1800).
---
## 2. Второстепенные — закрыто 5 из 8
| | Статус |
|---|---|
| S1 время сброса квоты | ✅ 7200 s для «resets in 2h» |
| S5 `py_compile` по `.venv` | ✅ обход ограничен `target_src` |
| S6 настройки не сохранялись | ✅ `_save_settings` читает состояние виджетов (`aff_sw.get()` и др.) |
| S8 сбор pytest падал целиком | ✅ `pytest.importorskip("customtkinter")`; прогон: **44 passed, 2 skipped, 3 deselected** |
| S9 опрос OAuth после закрытия | ✅ переопределён `destroy()` со сбросом `_polling_active` |
| **S2 канал обновлений** | ❌ не исправлено |
| **S3 подлинность пакета** | ❌ не адресовано |
| **S4 слепая проверка секретов** | ❌ не исправлено |
| S7 тест установщика меняет систему | ❌ не адресовано (риск низкий: `installer` deselected по умолчанию) |
---
## 3. Новое, найденное в этом раунде
### N1. Мастер: «Только резерв (Spare)» молча не срабатывает, а журнал рапортует успех
`add_account_wizard.py:373` предлагает вариант `spare`, строка 394 маппит его в `"spare"`, строка 401 вызывает `assign_profile_to_role(..., "spare", ...)`. После правки B2 эта роль **отклоняется** (`ok=False`), но возвращаемое значение игнорируется, и следом безусловно пишется:
```python
EventLogService.get().log("account", f"... Назначен на роль: {target_role}.", level="success")
```
Пользователь выбирает «Только резерв», видит успех — назначения не происходит. До B2 создавалась мусорная роль, теперь не делается ничего; в обоих случаях функция не работала, но раньше отказ был хотя бы виден в конфиге.
**Нужно:** либо добавить `spare` в `CANONICAL_ROLE_MAP` с осмысленной семантикой (добавление в хвост цепочки нескольких ролей), либо проверять результат и показывать пользователю отказ. `auto_assign_all` возвращаемое значение проверяет (`if ok:`) — здесь надо так же.
### N2. Двойной префикс в тексте ошибки (косметика)
`_error_completion` уже добавляет `"Antigravity error: "`, guard в `hermes_plugin`/`runtime` добавляет его повторно → `Antigravity error: Antigravity error: individual quota reached`.
### N3. Релизный гейт зелёный на непроверенных вещах
- Пункт «3. Auto-Updater & Rollback → PASS»: ни один тест в `test_updater.py` не обращается к реальному манифесту — все работают на инъецированном словаре или локальных файлах. Реальный канал при этом отдаёт **HTTP 404** (проверено). Гейт подтверждает механику, но не работоспособность обновления.
- Пункт «6. Zero Credentials & Secrets → PASS»: проверка ищет файлы (`auth.json`, `*.key`, `.env*`) и живые ключи по regex `sk-…`/`opencode-…`. `CLIENT_SECRET` в `oauth.py:29`, склеенный из фрагментов именно для обхода сканеров, не обнаруживается. Гейт будет зелёным при секрете в коде.
---
## 4. Остаточное (не блокирует merge, блокирует релиз)
**R1 (S2). Встроенный апдейтер нерабочий.** `update_manager.py:82` по-прежнему указывает на `raw.githubusercontent.com/ochenstarik-ui/hermes-hub/main/dist/update_manifest.json`. Проверено сейчас: `HTTP 404`, файл в git не отслеживается (`dist/` в `.gitignore`), репозиторий приватный. Пока не появится публичный канал (GitHub Releases API или отдельный репозиторий манифестов), «Проверить обновления» всегда будет падать.
**R2 (S3). Только целостность, без подлинности.** SHA-256 берётся из того же манифеста, что и `package_url`; `HERMES_HUB_UPDATE_URL` переопределяется переменной окружения; хост `package_url` не ограничен allowlist'ом. Для механизма, распаковывающего исполняемый код в рабочий каталог, нужна подпись пакета либо жёсткая привязка к домену релизов.
**R3 (из B3). Комментарии в YAML по-прежнему стираются:** 5 строк → 0 при первом сохранении. Данные и структура больше не теряются, но пользовательские пояснения — да. Решается либо переходом на `ruamel.yaml` с сохранением комментариев, либо явным предупреждением в документации, что файл перезаписывается приложением.
**R4.** Сохранённые настройки (`hub_settings.json`) по-прежнему никем не читаются: `failover_attempts`, `session_affinity`, `auto_failover`, `model_timeout_sec`, `monitoring_interval_min` не влияют на роутер. Переключатели теперь честно сохраняются — но эффекта не дают.
**Из аудита без изменений:** `_CM_LOCK` на всё время subprocess; глобальная запись `gemini:antigravity` без восстановления; session affinity без TTL; `router_state.json` без межпроцессной блокировки; roadmap-модули не подключены; веб-стек мёртв.
---
## 5. Рекомендация
1. **Merge `65482e8` в `main`** — блокеры закрыты, регрессий не внесено, тесты зелёные.
2. **До объявления v0.1.1:** закрыть N1 (одна строка + проверка результата), поднять реальный канал обновлений (R1) либо скрыть кнопку «Проверить обновления», научить гейт видеть склеенные секреты (N3/S4) — иначе «RELEASE GATE: PASSED» вводит в заблуждение.
3. N2, R3, R4 — в обычную очередь.

View file

@ -0,0 +1,100 @@
# Ревью, раунд 3: `925e6c6``42dfe2c` (N1N3, S2S4, R1R4)
**База:** `65482e8`**проверено:** `42dfe2c` (4 коммита)
**Тесты:** 50 passed, 2 skipped, 3 deselected. **Гейт:** PASSED (Candidate v0.1.1).
**Вердикт: ✅ Заявленное сделано и проверено. ⛔ Найден один новый блокер релиза — канонический установщик не содержит исправления P0-1.**
---
## 1. Закрыто (проверено исполнением)
**N1 — «Только резерв».** У `spare` появилась настоящая семантика, мастер проверяет результат:
```
ag-w1 в цепочках ДО : ['coder-primary']
spare -> ok=True Профиль 'ag-w1' сохранен в пуле резерва (spare)
ag-w1 в цепочках ПОСЛЕ: []
мусорная роль -> ok=False Неизвестная роль маршрутизатора
```
Мелочь: при `ok=False` мастер делает `return` без закрытия окна и без сообщения в самом окне — предупреждение уходит только в журнал, кнопка выглядит «мёртвой». Лучше показать текст ошибки в модалке.
**N2 — двойной префикс.** `Antigravity error: individual quota reached` — один префикс.
**N3 / S4 — сканер секретов.** AST-обход с раскрытием конкатенаций и `''.join(...)`, allowlist для документированных публичных констант. Проверил синтетическим файлом:
```
CLIENT_SECRET = "".join(("GOC","SPX","-","someothersecret123")) -> Obfuscated secret assignment ✅
API_KEY = "hardcoded-live-value-xyz" -> Hardcoded sensitive secret ✅
реальный oauth.py -> чисто (константы в allowlist)
```
Сканер больше не даёт ложного «zero secrets».
**OAuth — деобфускация.** Склейка из фрагментов убрана, константы записаны прямо, решение задокументировано в `docs/OAUTH_CLIENT.md` с моделью угроз по RFC 8252/7636 и явным признанием, что используется стандартный клиент Google CloudCode Desktop. Это ровно то, что запрашивалось: «собственный клиент либо документированное заимствование».
**S3 — allowlist хостов.** HTTPS-only + список доменов GitHub, применяется и к манифесту (`:117`), и к `package_url` при скачивании (`:174`).
**R1 / S2 — канал обновлений.** Фид переехал на отдельный публичный репозиторий, 404 обрабатывается мягко («Канал обновлений пока не настроен»), гейт честно печатает `[NOT PUBLISHED YET]` вместо голого PASS. Формулировка сменилась на «Ready for Candidate».
**R4 — настройки влияют на роутер.** `settings_service.py` + чтение в `route_request`. Проверил поведением:
```
auto_failover=false -> router_error=True, попыток в trail: 1 (failover действительно отключён)
```
Работают `auto_failover`, `session_affinity`, `failover_attempts`, `auto_return_primary`.
---
## 2. ⛔ Новый блокер релиза: исправление P0-1 не попало в канонический установщик
`installer/README.md` (новый файл) объявляет:
> **Canonical Binary:** `HermesHubSetup.exe` (built from `HermesHubSetup.cs`)
> `HermesHubSetup.py`: Internal development helper script… **Not distributed** as a primary installer artifact.
> `install.ps1`: Deprecated legacy bootstrap script.
При этом проверка кода:
| | `HermesHubSetup.cs` (канонический) | `HermesHubSetup.py` (объявлен вспомогательным) |
|---|---|---|
| Установка `customtkinter`/`pillow` в venv Hermes | **нет ни одного упоминания** | есть (6 упоминаний, `pip install` + проверка импорта) |
| Копирование `assets/branding` | **нет** | есть |
`build_installer.ps1` собирает `dist/HermesHubSetup.exe` именно из `.cs`.
Итог: архитектурное решение объявило каноническим тот установщик, где P0-1 **не исправлен**, а тот, где исправлен, — недистрибутируемым. По канонической цепочке установки приложение по-прежнему не запустится (`pythonw.exe` молча завершится на `import customtkinter`), и логотипов не будет.
Тест `test_p0_1_installer_dependencies` этого не ловит: он проверяет импортируемость `customtkinter` в текущем runtime, а не то, что канонический установщик его ставит.
**Нужно одно из двух:** перенести установку зависимостей и копирование assets в `HermesHubSetup.cs`, либо объявить каноническим `.py` и пересобрать конвейер под него. Плюс тест, проверяющий именно канонический артефакт.
---
## 3. Заявлено, но кодом не подтверждено
**S7.** В таблице отчёта: «unit tests isolated from Windows Registry/Start Menu». Фактически `tests/test_installer.py` **не менялся** с раунда 1 и по-прежнему делает `subprocess.run([SETUP_EXE, "/silent"])` (строки 53, 68). Песочница через `env` закрывает только файловое дерево; `CreateStartMenuShortcut` пишет в `%APPDATA%\…\Start Menu`, `RegisterInWindowsUninstall` — в `HKCU`. Это политика в `installer/README.md`, а не изоляция в коде. Риск низкий (маркер `installer` deselected по умолчанию), но формулировку в отчёте стоит поправить.
---
## 4. Мелкие замечания
**M1.** `is_allowed_update_host(url, allow_dev_local=True)` вызывается с `allow_dev_local=True` в обоих местах (`:117`, `:174`). Внутри: `if url.startswith("file://") or Path(url).exists(): return allow_dev_local or os.environ.get("HERMES_HUB_DEV_MODE") == "1"`. При `allow_dev_local=True` условие по переменной окружения не проверяется вообще — гейт dev-режима, который вы написали, обходится собственными вызовами. Локальный путь в `package_url` пройдёт allowlist в продакшене.
**M2.** Гейт: заголовок «2. P0 Release Blockers (**16/16**)», сообщение — «**12/12** P0 release blockers verified». Косметическое расхождение.
**M3.** `get_hub_settings()` читается с диска на **каждый** `route_request` — файловый I/O на запрос. Стоит кэшировать с инвалидацией по mtime.
**M4.** Репозиторий `ochenstarik-ui/hermes-hub-releases` пока не существует (проверено: `gh repo view` → not found, манифест 404). Ожидаемо для кандидата, но до релиза его нужно создать и опубликовать манифест.
---
## 5. Остаточное из прошлых раундов
- **R3.** Комментарии в `router_profiles.yaml` по-прежнему стираются при сохранении (5 → 0). Данные и структура целы.
- **R4 частично.** `model_timeout_seconds`, `monitoring_interval_seconds`, `auto_monitoring` сохраняются, но нигде не читаются — работают 4 настройки из 7.
- Без изменений: `_CM_LOCK` на всё время subprocess; глобальная запись `gemini:antigravity` без восстановления; session affinity без TTL; `router_state.json` без межпроцессной блокировки; roadmap-модули не подключены; веб-стек мёртв.
---
## 6. Рекомендация
1. **Merge — да.** Регрессий не внесено, тесты и гейт зелёные, качество ответа на замечания высокое.
2. **Кандидатом v0.1.1 объявлять после закрытия блокера из раздела 2** — иначе установленное каноническим установщиком приложение не стартует.
3. M1 — быстро и по делу (убрать `allow_dev_local=True` из продакшен-вызовов). M2, M3, S7-формулировка — в обычную очередь.

View file

@ -0,0 +1,120 @@
# Ревью, раунд 4: вердикт по кандидату v0.1.1
**Отчёт исполнителя описывает `8314d46`. Фактический `origin/main``3aae1a8`, на 3 коммита впереди.**
Проверка проводилась по фактическому HEAD.
**Вердикт: ⛔ тег `v0.1.1` не выпускать.** Заявленное в отчёте сделано и подтверждено, но на текущем HEAD релизный гейт **падает**, а публичный фид обновлений указывает на несуществующий пакет.
---
## 1. Заявленное в отчёте — подтверждаю
| Пункт | Проверка | Итог |
|---|---|---|
| Канонический `.cs`: `EnsurePythonDependencies` | `installer/HermesHubSetup.cs:89` — проверка импорта → `ensurepip``pip` → фолбэк на `uv` → повторная верификация | ✅ |
| Канонический `.cs`: копирование assets | строки 263269, три целевых каталога (install dir, плагин, hermes home) | ✅ |
| Post-install smoke test | строка 303, `HERMES_HUB_IMPORT_OK`, проверяется ExitCode и наличие маркера | ✅ |
| M1 — dev-bypass | `allow_dev_local` по умолчанию `False`, оба продакшен-вызова передают `False` явно (`:130`, `:187`) | ✅ |
| M2 — счётчик гейта | `16/16` в заголовке и в сообщении согласованы | ✅ |
| M3 — кэш настроек | mtime-кэш + `invalidate_settings_cache()` при сохранении | ✅ |
| Публичный фид | `HTTP 200`, манифест валидный, v0.1.1 | ✅ |
Блокер раунда 3 (исправление P0-1 не в каноническом установщике) **закрыт**.
---
## 2. ⛔ Блокер 1: релизный гейт падает на текущем HEAD
Отчёт цитирует `[RELEASE GATE: PASSED]` — это результат для `8314d46`. На `3aae1a8` фактически:
```
[RELEASE GATE: FAILED] One or more checks failed. Release blocked.
```
Причина — новый `tests/test_codex_opencode_wizard.py` из коммита `3aae1a8` импортирует `customtkinter` на уровне модуля **без `pytest.importorskip`**, и сбор всей сессии прерывается:
```
ERROR tests/test_codex_opencode_wizard.py
!!! Interrupted: 1 error during collection !!!
1 skipped, 3 deselected, 1 error
```
Падают пункты гейта 2, 3 и 4. Это возврат ровно того дефекта, который закрывался как **S8** в раунде 2: отсутствие опциональной UI-зависимости должно давать *skip*, а не обрывать прогон. Остальные три тестовых модуля (`test_ui_refinement`, `test_p0_release_gate`, `test_oauth_lifecycle`) `importorskip` содержат — забыли только в новом.
Без этого файла суite зелёный: **57 passed, 4 skipped, 3 deselected**. Исходный код, который тестируется (`CodexOAuthSession`, `enable_clipboard_shortcuts`, `HubEntry`, `extract_jwt_identity`), на месте.
**Оговорка:** на машине, где `customtkinter` установлен, гейт пройдёт. Но инвариант «отсутствие UI-зависимости не ломает прогон» нарушен — а это ровно тот сценарий, ради которого существует P0-1.
**Правка:** одна строка `pytest.importorskip("customtkinter")` в новом файле.
---
## 3. ⛔ Блокер 2: фид обновлений указывает на несуществующий пакет
Манифест живой и отдаёт `HTTP 200`, но пакет из него — нет:
```
package_url: https://github.com/ochenstarik-ui/hermes-hub-releases/releases/download/v0.1.1/hermes-hub-0.1.1.zip
-> HTTP 404
gh release list --repo ochenstarik-ui/hermes-hub-releases -> пусто
```
Пункт гейта 7 проверяет только наличие полей `version` и `package_url` в манифесте, но не достижимость самого пакета — поэтому печатает `[LIVE STATUS] Public update manifest live`. Формально верно, по существу — обновление скачать нельзя.
Сейчас это не проявится (манифест объявляет 0.1.1, приложение 0.1.1 → «обновлений нет»), но как только выйдет 0.1.2, пользователь получит ошибку загрузки.
**Правка:** опубликовать релизный ассет с заявленным `sha256`, а в `check_production_update_feed` добавить HEAD-запрос по `package_url`.
---
## 4. Артефакт релиза непроверяем из репозитория
Отчёт приводит SHA256 собранного установщика `1D41B9D6…`. Проверить его нельзя:
```
файлов dist/ в git: 0 (dist/ в .gitignore)
локальный dist/HermesHubSetup.exe: 7e2531… (сборка от 20.08 00:16, устаревшая)
dist/checksums.txt: содержит старую сумму 7E2531…
```
Собранный установщик существует только на машине исполнителя. Любой, кто клонирует репозиторий, установщика не получает, а `checksums.txt` вводит в заблуждение. Для релиза артефакт должен попасть в GitHub Release (там же, где ассет обновления), а `checksums.txt` — обновляться сборочным скриптом.
Заявленный clean-install тест в изолированном venv я подтвердить не могу — воспроизвести его без артефакта нечем. Код установщика при этом проверен и выглядит корректно (см. раздел 1).
---
## 5. Отчёт отстал от кода на 2097 строк
После `8314d46` в `main` влиты три коммита, которых отчёт не касается:
```
0d9005f fix(oauth): eliminate ERR_CONNECTION_REFUSED, immediate listener binding, single session reuse
249a888 feat(oauth): Cockpit Tools model, manual callback fallback, race protection
3aae1a8 feat(wizard): Codex OAuth/API Key & OpenCode Go with clipboard UX
```
`+2097 / 232` строк: новый `codex_oauth.py` (296), переработанный `profile_oauth.py` (+296), `add_account_wizard.py` (+880), `components.py` (+83), два новых тестовых модуля.
Это функциональная разработка, а не закрытие замечаний, и она не отревьюирована. Беглая проверка на утечки: в `codex_oauth.py` и `profile_oauth.py` токены и коды в логи не пишутся — логируются только `source` и HTTP-код. Полноценного ревью этих коммитов не проводилось.
**Вердикт по релизу не может покрывать неотрецензированный код.** Либо тег ставится на `8314d46` (после закрытия блокера 2), либо новые коммиты идут на отдельное ревью.
---
## 6. Условия выпуска v0.1.1
1. Добавить `pytest.importorskip("customtkinter")` в `tests/test_codex_opencode_wizard.py`; убедиться, что гейт зелёный в окружении **без** UI-зависимостей.
2. Опубликовать релизный ассет `hermes-hub-0.1.1.zip` с суммой из манифеста; расширить проверку 7 HEAD-запросом по `package_url`.
3. Положить `HermesHubSetup.exe` в GitHub Release и обновить `dist/checksums.txt` сборочным скриптом.
4. Определиться с областью тега: `8314d46` либо отдельное ревью коммитов `0d9005f…3aae1a8`.
Пункты 1 и 2 — блокирующие. 3 и 4 — обязательны для честного релиза, но могут быть закрыты процедурно.
---
## 7. Остаточное из прошлых раундов (не блокирует)
- Комментарии в `router_profiles.yaml` стираются при сохранении.
- `model_timeout_seconds`, `monitoring_interval_seconds`, `auto_monitoring` сохраняются, но не читаются (работают 4 настройки из 7).
- `tests/test_installer.py` по-прежнему пишет в Start Menu и `HKCU` при запуске с `-m installer`.
- `_CM_LOCK` на всё время subprocess; глобальная запись `gemini:antigravity` без восстановления; session affinity без TTL; `router_state.json` без межпроцессной блокировки; roadmap-модули не подключены; веб-стек мёртв.

View file

@ -0,0 +1,123 @@
# Ревью, раунд 5: `0d9005f…0c511cd` (OAuth, мастер, тарифы, квоты, Claude/Grok, Plan A)
**База:** `8314d46`**проверено:** `0c511cd` (5 коммитов, +6970 / 665)
**Состояние:** тесты 91 passed / 7 skipped / 3 deselected, релизный гейт зелёный.
**Вердикт: ⛔ два блокирующих дефекта — фабрикация данных о квотах и поддельный код авторизации. Оба относятся к классу «пользователь видит правдоподобное, но выдуманное».**
---
## 1. ⛔ Блокер A: сборщик квот выдаёт захардкоженные числа под видом данных провайдера
`src/antigravity_provider/router/quota_collector.py` (561 строка) **не делает ни одного сетевого вызова**:
```
urlopen вызовов: 0
```
При этом каждый сборщик возвращает фиксированные константы и помечает их источником провайдерского API:
```python
# _collect_antigravity_quota, строки 320346
QuotaBucket(id="antigravity.gemini.5h", used_percent=9.0, remaining_percent=91.0, status="healthy")
QuotaBucket(id="antigravity.gemini.weekly", used_percent=1.0, remaining_percent=99.0, status="healthy")
...
return QuotaSnapshot(..., source="antigravity_api")
```
Всего **17 литеральных `used_percent`** по пяти провайдерам, с источниками:
`antigravity_api`, `codex_usage_api`, `opencode_api`, `claude_oauth_usage_api`, `xai_task_usage_api`.
Эти значения доходят до пользователя: `ui/views/accounts_view.py:168`
```python
snap = quota_snap or p.quota_snapshot or AccountQuotaService.get().get_snapshot(p.provider, p.profile_id)
```
и рисуются в `quota_box` карточки аккаунта. То есть на экране «Аккаунты» показывается «Gemini 5h — использовано 9%, осталось 91%» независимо от реального состояния квоты. Ровно та задача, ради которой существует продукт — понимать, где кончилась квота, — решается вымышленными числами.
**Тесты фикцию не закрепляют** (`test_accounts_tariffs_quotas.py` проверяет арифметику `used/remaining` на собственных значениях), поэтому дефект не виден по зелёному прогону.
**Требуется одно из двух:**
- реализовать реальный сбор (запрос к провайдеру) — тогда источники честные;
- либо пометить снапшоты как `source="baseline"`/`"estimated"` и явно показать в UI, что это оценка, а не данные провайдера. Значение `"baseline"` в коде уже есть (строка 525) — значит, различие осознавалось.
---
## 2. ⛔ Блокер B: тихий фолбэк на поддельный код авторизации
`codex_oauth.py:103112`:
```python
try:
resp = _post_json(CODEX_OAUTH_USER_CODE_URL, {"client_id": CODEX_OAUTH_CLIENT_ID})
self.user_code = resp.get("user_code")
self.device_auth_id = resp.get("device_auth_id")
except Exception as e:
logger.warning("Could not reach OpenAI deviceauth endpoint directly: %s. Using local session.", e)
self.user_code = f"CDX-{secrets.token_hex(3).upper()}"
self.device_auth_id = secrets.token_urlsafe(16)
```
То же в `grok_oauth.py:9194` (`GRK-XXXXXX` + локальный `device_code`).
По RFC 8628 `user_code` и `device_code` выдаёт **сервер авторизации**. Сгенерированные локально, они серверу неизвестны. Последствие: при любой недоступности эндпоинта (сетевой сбой, смена API, гео-блокировка) мастер показывает пользователю правдоподобный код вида `CDX-A1B2C3`, предлагает ввести его на странице провайдера, затем поллит `device_auth_id`, которого не существует, — и через 15 минут выдаёт таймаут. Отличить подделку от настоящего кода пользователь не может; в логах остаётся только `warning`.
Комментарий в коде говорит «for offline or simulated/mocked environment» — то есть режим предназначен для тестов, но срабатывает в продакшене по любому исключению.
**Требуется:** при неудаче запроса кода показывать ошибку немедленно, а фолбэк включать только под явным флагом (`HERMES_HUB_DEV_MODE=1`), по образцу уже сделанного в `update_manager.is_allowed_update_host`.
---
## 3. Существенные замечания
**S1. Три новых заимствованных OAuth-клиента без документирования.**
```
codex_oauth.py:31 CODEX_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann" (OpenAI Codex CLI)
grok_oauth.py:30 XAI_OAUTH_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828"
claude_oauth.py клиент Claude
```
Для Google было принято решение и оформлено `docs/OAUTH_CLIENT.md` с моделью угроз по RFC 8252. Три новых клиента появились без такого же разбора. Политику нужно распространить на них — иначе документ описывает одну треть реальной картины.
**S2. `gui_server.py` вырос на 112 строк и по-прежнему ничем не запускается.** Проверено: ни один `.py`, `.bat`, `.ps1`, `.cs` не вызывает `run_gui_server`. В мёртвый параллельный стек вкладываются усилия; `fastapi`/`uvicorn` остаются обязательными зависимостями.
**S3. `health_tracker`: атомарность есть, межпроцессной блокировки нет.** Запись через `tempfile.mkstemp` + `os.replace` (строки 136145) — риск порванного файла закрыт. Но GUI-процесс и процесс Hermes по-прежнему перетирают состояние друг друга целиком (last-writer-wins).
---
## 4. Закрыто в этих коммитах (подтверждаю)
| Замечание прошлых раундов | Статус |
|---|---|
| Session affinity без TTL и вытеснения | ✅ `ttl_seconds=1800`, `max_entries=1000`, `prune_expired()` |
| `_restore_status` блокирует UI сетевым сканом | ✅ читает готовый снапшот из `HubStateStore`, сети в UI-потоке нет |
| Новые модули оказываются мёртвым кодом | ✅ не повторилось: `quota_collector` (9 импортёров), `state_store` (8), `account_identity` (5), `scheduler` (5), `event_bus` (4), `model_registry` (2) — всё подключено |
| Новые адаптеры не регистрируются | ✅ `claude` и `grok` есть в `_ADAPTERS` |
| Наложение периодических задач | ✅ `HermesRefreshScheduler` с overlap-skip и счётчиком `tasks_skipped_overlap` |
| Утечка секретов в логи | ✅ не найдено: в `add_account_wizard.py`, `codex_oauth.py`, `grok_oauth.py`, `claude_oauth.py` токены и ключи не логируются; в мастере поле с `show="*"` и маскирование идентичности |
Отдельно отмечу качество: капабилити-роутинг в `router_engine` со скорингом моделей и фолбэком в пределах аккаунта сделан аккуратно, `state_store` с дельта-обновлениями — правильное архитектурное решение против прежних полных пересканов.
---
## 5. Остаётся открытым с прошлых раундов
- `capability_matrix`, `lifecycle_supervisor`, `skill_registry`, `deepseek_adapter` — по-прежнему не подключены ни к одному кодовому пути.
- Комментарии в `router_profiles.yaml` стираются при сохранении.
- `model_timeout_seconds`, `monitoring_interval_seconds`, `auto_monitoring` сохраняются, но не читаются.
- `tests/test_installer.py` при запуске с `-m installer` пишет в Start Menu и `HKCU`.
- `_CM_LOCK` удерживается на всё время subprocess `agy`; глобальная запись `gemini:antigravity` перезаписывается без восстановления.
---
## 6. Условия приёмки
**Блокирующие:**
1. Блокер A — либо реальный сбор квот, либо честная маркировка `estimated`/`baseline` в источнике и в UI.
2. Блокер B — фолбэк на локальный код только под `HERMES_HUB_DEV_MODE=1`, иначе немедленная ошибка.
**До релиза:**
3. Распространить решение по OAuth-клиентам на OpenAI, xAI и Claude в `docs/OAUTH_CLIENT.md`.
4. Определиться с `gui_server.py`: удалить, вынести в `legacy/` или подключить.
После закрытия 1 и 2 препятствий к тегу со стороны кода не вижу — остаются организационные пункты из раунда 4 (опубликовать ассет релиза и `HermesHubSetup.exe`, обновить `checksums.txt`).

View file

@ -0,0 +1,87 @@
# Задание: Правдивость данных и готовность к релизу v0.1.1
## Дата поступления
2026-08-21
## Контекст
По итогам ревью раунда 5 (`0d9005f…0c511cd`, +6970 строк) выявлены два блокирующих дефекта одного класса: **пользователю показываются правдоподобные, но выдуманные данные**. Тесты и релизный гейт при этом зелёные — дефекты не детектируются существующими проверками.
Ветка `main` = `0c511cd`. Тег `v0.1.1` снят с `origin` и локально (указывал на `65482e8` без исправления P0-1).
**Просьба: на время выполнения этого задания заморозить `main` для новой функциональности.** За два предыдущих ревью в ветку влилось ~7300 строк, из-за чего вердикт устаревал к моменту выдачи.
---
## Область задачи (Scope)
### P0-1. Сборщик квот не должен выдавать выдуманные числа за данные провайдера
`src/antigravity_provider/router/quota_collector.py` не делает ни одного сетевого вызова (`urlopen`: 0), но возвращает 17 захардкоженных `used_percent` и помечает снапшоты источниками `antigravity_api`, `codex_usage_api`, `opencode_api`, `claude_oauth_usage_api`, `xai_task_usage_api`. Эти значения выводятся в карточке аккаунта (`ui/views/accounts_view.py:168`).
Выбрать один из путей:
**Путь А (предпочтительный) — реальный сбор.** Запрашивать потребление у провайдера там, где есть API; при успехе оставлять источник `*_api`, при неудаче — переключаться на путь Б.
**Путь Б — честная маркировка.** Если данные вычисляются локально:
- источник снапшота — `estimated` или `baseline`, но **не** `*_api`;
- в UI рядом с корзиной квоты — явный признак оценки (значок/подпись «оценка», не проверено у провайдера);
- значения не должны выглядеть как измерение: либо диапазон, либо «нет данных» вместо `9%`.
Смешанный вариант (А с фолбэком на Б) допустим и желателен.
### P0-2. Убрать тихий фолбэк на поддельный код авторизации
`router/codex_oauth.py:103112` и `router/grok_oauth.py:9194` при недоступности эндпоинта провайдера генерируют локальные `user_code` (`CDX-…`, `GRK-…`) и `device_code`/`device_auth_id`. По RFC 8628 их выдаёт сервер авторизации; локально сгенерированные ему неизвестны, поллинг гарантированно завершается таймаутом через 15 минут, а пользователь всё это время видит правдоподобный код.
Требуется:
- в обычном режиме при неудаче запроса кода — **немедленная ошибка** с текстом причины в мастере, без запуска поллинга;
- фолбэк на локальную сессию — только под явным `HERMES_HUB_DEV_MODE=1` (по образцу `update_manager.is_allowed_update_host`), с видимой пометкой в UI, что сессия тестовая;
- проверить остальные OAuth-модули (`claude_oauth.py`, `profile_oauth.py`) на такой же паттерн.
### P1-3. Распространить решение по OAuth-клиентам на новых провайдеров
`docs/OAUTH_CLIENT.md` описывает только Google. В коде появились ещё три заимствованных клиента:
```
codex_oauth.py:31 app_EMoamEEZ73f0CkXaXp7hrann (OpenAI)
grok_oauth.py:30 b1a00492-073a-47ea-816f-4c329264a828 (xAI)
claude_oauth.py клиент Claude
```
Добавить их в документ с той же структурой: тип клиента, модель угроз, обоснование по RFC 8252/7636, явное указание происхождения.
### P1-4. Определить судьбу `gui_server.py`
Файл вырос ещё на 112 строк, но `run_gui_server` не вызывается ниоткуда (проверено по `.py`, `.bat`, `.ps1`, `.cs`). Либо подключить, либо вынести в `legacy/`, либо удалить вместе с `gui_cockpit.html`. При удалении — убрать `fastapi`/`uvicorn` из обязательных зависимостей `pyproject.toml`.
### P1-5. Организационные пункты релиза (из раунда 4)
1. Опубликовать GitHub Release в `ochenstarik-ui/hermes-hub-releases`: ассет `hermes-hub-0.1.1.zip` с суммой, совпадающей с `sha256` в `update_manifest.json` (сейчас `package_url` → HTTP 404).
2. Положить туда же `HermesHubSetup.exe`; `dist/checksums.txt` обновлять сборочным скриптом, а не вручную.
3. Тег `v0.1.1` ставить только на коммит, прошедший ревью. Если локальный тег остался на машине исполнителя — удалить: `cd <repo>; git tag -d v0.1.1`.
### P2-6. Долг, накопленный за пять раундов (в обычную очередь)
- `capability_matrix`, `lifecycle_supervisor`, `skill_registry`, `deepseek_adapter` — не подключены ни к одному кодовому пути; подключить либо пометить экспериментальными.
- Комментарии в `router_profiles.yaml` стираются при сохранении.
- `model_timeout_seconds`, `monitoring_interval_seconds`, `auto_monitoring` сохраняются, но не читаются (работают 4 настройки из 7).
- `tests/test_installer.py` при `-m installer` пишет в Start Menu и `HKCU`.
- `_CM_LOCK` удерживается на всё время subprocess `agy`; глобальная запись `gemini:antigravity` перезаписывается без восстановления.
- `health_tracker`: запись атомарна, но межпроцессной блокировки нет (last-writer-wins между GUI и Hermes).
---
## Критерии приёмки
Ревью будет проверять ровно это, исполнением:
1. `grep -c urlopen quota_collector.py` > 0 **либо** ни один снапшот с локально вычисленными значениями не имеет источник `*_api`. Проверяется вызовом сборщика и чтением поля `source`.
2. В UI карточки аккаунта для оценочных данных присутствует визуальный признак оценки.
3. При недоступности эндпоинта устройства (мокируется исключением в `_post_json`) и **без** `HERMES_HUB_DEV_MODE`: `start()` возвращает ошибку, `status != "pending"`, поток поллинга не стартует.
4. С `HERMES_HUB_DEV_MODE=1` фолбэк работает и помечен в UI.
5. `docs/OAUTH_CLIENT.md` содержит разделы по OpenAI, xAI и Claude.
6. `run_gui_server` либо вызывается, либо файла нет в `src/`.
7. `curl -I <package_url из манифеста>` → HTTP 200, sha256 совпадает.
8. Регрессия не внесена: полный `pytest tests/` зелёный **в окружении без UI-зависимостей** (проверка инварианта `importorskip`), релизный гейт PASSED.
9. Новые тесты на P0-1 и P0-2, падающие на текущем `0c511cd`.
## Что НЕ входит в задание
Новая функциональность. Задание закрывает дефекты и готовит релиз; любые новые возможности — после тега.