hermes-hub/agents/inbox/2026-08-20-review-round5-unreviewed-features.md
Hermes Team 46a185395c 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>
2026-08-20 22:22:57 +07:00

123 lines
10 KiB
Markdown
Raw Permalink 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.

# Ревью, раунд 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`).