A11 (Pro, тяжёлая ветка): интеграция с Hermes — перехват без роли, граница между учётными системами Hub и Hermes, токены Codex и безопасное переключение аккаунта, замена зонда обнаружения моделей на agy models, устранение выдуманного запасного списка в адаптере. A12 (Flash, быстрая ветка): компактные карточки аккаунтов, разбор дублирующих разделов «Провайдеры»/«Квоты», причина у каждого Н/Д, тест на достижимость службы обнаружения моделей, живое подключение Grok. Заданы строгие непересекающиеся списки файлов, чтобы параллельная работа не дала конфликтов. В A12 порядок работы с git расписан пошагово: pull в начале, push ветки сразу после первого коммита, обязательная проверка финального push через git log origin/<ветка>. A10 удалено — заменено этой парой. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
252 lines
21 KiB
Markdown
252 lines
21 KiB
Markdown
# Задание A11 (Antigravity Pro, этот ПК): интеграция с Hermes и учётные данные
|
||
|
||
## Дата поступления
|
||
2026-08-23
|
||
|
||
## База
|
||
Проверочный HEAD на момент выдачи: **`cdfd9f1`**.
|
||
|
||
## Ветка
|
||
`antigravity/hermes-integration`
|
||
|
||
## Кому
|
||
Тяжёлая ветка работ: разбор чужого кода, обратная разработка эндпоинтов, проектные решения. Механических правок здесь нет.
|
||
|
||
---
|
||
|
||
## Порядок работы с git
|
||
|
||
**В начале — обновить локальную копию:**
|
||
|
||
```
|
||
cd <каталог репозитория>; git fetch origin --prune; git status
|
||
git checkout main; git pull --ff-only origin main
|
||
git checkout -b antigravity/hermes-integration
|
||
```
|
||
|
||
Зафиксировать фактический `BASE_SHA` через `git rev-parse --short HEAD`. Не считать `cdfd9f1` актуальным автоматически — параллельно идёт задание A12.
|
||
|
||
**Ветку отправить в `origin` сразу после первого коммита**, не дожидаясь готовности:
|
||
|
||
```
|
||
git push -u origin antigravity/hermes-integration
|
||
```
|
||
|
||
**В конце — обязательный push:**
|
||
|
||
```
|
||
git push origin antigravity/hermes-integration
|
||
```
|
||
|
||
Работа, которой нет в `origin`, для проекта не существует: проверка идёт исполнением, а не по отчёту.
|
||
|
||
---
|
||
|
||
## Параллельная работа: строгая граница по файлам
|
||
|
||
Одновременно выполняется **A12** вторым исполнителем.
|
||
|
||
**Ваши файлы:**
|
||
|
||
```
|
||
src/antigravity_provider/hermes_plugin.py
|
||
src/antigravity_provider/router/router_engine.py
|
||
src/antigravity_provider/router/codex_oauth.py
|
||
src/antigravity_provider/router/profile_manager.py
|
||
src/antigravity_provider/router/quota_collector.py
|
||
src/antigravity_provider/router/adapters/**
|
||
src/antigravity_provider/agy_subprocess.py
|
||
docs/UI_STATE_CONTRACT.md
|
||
tests/test_integration_*.py (новые файлы)
|
||
```
|
||
|
||
**Не ваши** (зона A12): `router/ui/**`, `router/model_discovery.py`, `router/model_discovery_service.py`, `router/router_config.py`, `installer/**`, `tests/test_ui_*.py`.
|
||
|
||
Понадобился чужой файл — скажите, будет заказан. Молча не трогать: в проекте уже был случай, когда два исполнителя переписали один файл и слияние дало конфликт в двух местах.
|
||
|
||
---
|
||
|
||
## Что принято по A9
|
||
|
||
Проверено исполнением на живой машине владельца — работа хорошая:
|
||
|
||
- **миграция конфигурации работает**: 16 → 22 профиля, `claude` и `grok` получили по три слота, `find_free_slot` возвращает существующие профили по всем пяти провайдерам. Это снимает корень жалобы «при подключении грока ошибка»;
|
||
- резервная копия `router_profiles.yaml.bak_<ts>` создаётся, **десять профилей antigravity не изменены ни в одном поле**, комментарии не потеряны;
|
||
- квоты `grok` и `opencode-go` честно отдают `None` с причиной вместо правдоподобных чисел;
|
||
- флаг `/repair` и `/reinstall` задействован, предупреждение CS0219 исчезло;
|
||
- граница зоны Codex не нарушена, правка плагина `2d62d39` сохранена.
|
||
|
||
**Исправлено ревьюером при слиянии** — переделывать не нужно, но знать полезно:
|
||
|
||
1. Служба обнаружения моделей была **недостижима**. Создан `model_discovery_service.py`, а интерфейс импортирует `model_discovery`. Импорт обёрнут в `except ImportError`, поэтому расхождение не давало ошибки — выбор моделей просто оставался пустым навсегда. Добавлена согласованная точка входа.
|
||
2. Служба отдаёт `discovered_at`, каталог искал `fetched_at`. Сведено.
|
||
3. Тест `test_ui_routing_graph.py` закреплял выдуманный список `grok-3`; вы верно убрали литералы, и тест начал падать. Приведён к честному поведению.
|
||
|
||
Урок на будущее: **защитный `except ImportError` прячет несобранную интеграцию.** Когда пишете модуль для чужого потребителя — согласуйте путь и проверьте импорт исполнением, а не глазами.
|
||
|
||
Два числа из отчёта A9, которые не сошлись: «218 passed, 29 skipped» против **307 passed, 2 skipped** на слитом `main`, и «Release Gate 7/7 PASSED» против **FAILED** в моём замере. Двадцать девять пропусков означают, что UI-тесты у вас не выполнялись. Прогон обязателен в окружении **с** `customtkinter`, `pillow`, `psutil`.
|
||
|
||
---
|
||
|
||
## P0-1. Hub перехватывает каждый вызов Hermes и назначает ему роль `orchestrator`
|
||
|
||
Самое важное в задании.
|
||
|
||
Владелец сообщил: «зашёл в Гермеса, а там наш хаб не работает, основной оркестратор не выбрался», и заключил, что Hub к Hermes не привязан. Заключение неверное, положение хуже: **Hub привязан и активно ломал Hermes.**
|
||
|
||
Установлено разбором кода Hermes и его журналов:
|
||
|
||
1. Плагин регистрируется штатно. `hermes_cli/plugins.py:4789` берёт `register` у модуля, корневой `__init__.py` его экспортирует, `ctx.register_middleware("llm_execution", …)` — допустимое имя (`hermes_cli/middleware.py:23`). Перехватчик **срабатывает на каждом обращении к модели**, это видно в трейсбеке `agent.log`.
|
||
|
||
2. **Hermes не передаёт роль.** `agent/conversation_loop.py:2950` передаёт `task_id`, `turn_id`, `api_request_id`, `session_id`, `platform`, `model`, `provider`, `base_url`, `api_mode`, `api_call_count`. Ключа `role` нет.
|
||
|
||
3. `resolve_role` доходит до последней строки и возвращает `config.default_role`. **Каждый вызов Hermes идёт как `orchestrator`.**
|
||
|
||
4. Цепочка `orchestrator` у владельца исчерпана целиком:
|
||
|
||
```
|
||
ag-orch-fallback skipped_unhealthy
|
||
codex-orch 429: Your account is not active, please check your billing details
|
||
opengo-3 No API key found for OpenCode Go profile 'opengo-3'
|
||
ag-w1, ag-w3 Antigravity error: agy error: authentication failed or timed out
|
||
```
|
||
|
||
5. Роутер возвращал «⚠️ Hermes Router Failover Exhausted» **как ответ ассистента**, и Hermes показывал это вместо ответа модели, хотя его собственный провайдер работал.
|
||
|
||
Следствие устранено ревьюером (`2d62d39`): при `router_error` вызов уходит вниз через `next_call`, отказ пишется в журнал уровнем `warning`. Закрыто тестом `tests/test_plugin_passthrough.py`. **Правку не откатывать.**
|
||
|
||
Принцип, который она закрепляет: **плагин может улучшить маршрутизацию, но не имеет права сделать Hermes хуже, чем без него.**
|
||
|
||
**От вас — причина.** Сейчас Hub на каждом вызове Hermes сначала пробует цепочку `orchestrator`: лишняя задержка и расход квоты не той роли даже там, где пропуск сработал верно.
|
||
|
||
1. **Определить роль честно.** Разобрать, что из переданного Hermes пригодно как признак: `task_id`, `session_id`, `platform`, `model`, `provider`. Надёжного признака нет — **не угадывать**. Эвристика в `resolve_role`, ищущая в системном сообщении подстроки «developer», «coding agent», «review agent», — это гадание по тексту промпта, и оно тоже подлежит пересмотру.
|
||
|
||
2. **Не претендовать на вызов без роли.** Роль не определена достоверно — пропускать вниз сразу, не тратя попыток. Роль по умолчанию для внешнего перехвата — неверная модель поведения.
|
||
|
||
**Тесты:** вызов без определяемой роли уходит вниз, не тратя попыток роутера; вызов с определённой ролью маршрутизируется; отказ цепочки никогда не возвращается как ответ ассистента.
|
||
|
||
## P0-2. Граница между учётными системами Hub и Hermes
|
||
|
||
Владелец прав по существу: Hub профилями Hermes не управляет.
|
||
|
||
Hermes ведёт собственные профили в каталоге `profiles` своего домашнего каталога:
|
||
|
||
```
|
||
agy-01 … agy-06, worker-fast, worker-research, worker-review,
|
||
worker-code, worker-code-2, deepseek
|
||
```
|
||
|
||
и настраивает `delegate_task` отдельно (`max_concurrent_children=3`, `provider=opencode-go`, `model=kimi-k2.7-code`).
|
||
|
||
Профили Hub — `ag-w1`, `ag-orch-fallback`, `codex-orch`, `opengo-*` — **другое множество идентификаторов**. Один и тот же аккаунт Google живёт в двух учётных системах под разными именами.
|
||
|
||
**Требуется:**
|
||
|
||
1. Раздел в `docs/UI_STATE_CONTRACT.md`: что Hub видит от Hermes, чего не видит, чем управляет и чем не управляет. Без этого интерфейс, показывающий «команду агентов», вводит владельца в заблуждение — он видит роли, которых Hermes не спрашивает.
|
||
|
||
2. **Варианты связывания профилей с оценкой цены каждого:** сопоставление по идентичности аккаунта (email из `id_token`), чтение профилей Hermes как источника, либо явная таблица соответствия. **Решение принимает владелец — вам подготовить варианты, не реализовывать молча.**
|
||
|
||
## P0-3. Codex: обновление токена и безопасное переключение аккаунта
|
||
|
||
Владелец прислал, как это делает Cockpit Tools, и просит так же:
|
||
|
||
```
|
||
1. Прочитать данные аккаунта access_token · id_token · refresh_token
|
||
2. Проверить access_token действителен до 28.08, обновление не требуется
|
||
3. Проверить id_token истёк 5 дней назад — нужно обновление
|
||
4. Обновить данные входа полный набор обновлён и сохранён
|
||
5. Остановить прежний процесс безопасная остановка ChatGPT/Codex и app-server
|
||
6. Записать данные клиента
|
||
7. Синхронизировать настройки
|
||
8. Запустить клиент Codex
|
||
```
|
||
|
||
Ключевое: токены проверяются **по отдельности**, и клиент останавливается **до** подмены учётных данных.
|
||
|
||
В отчёте A9 упомянуты токены Codex, но проверить это исполнением не удалось: у профилей Codex на машине владельца нет авторизации. Поэтому пункт остаётся открытым и должен быть закрыт **тестами**, а не только кодом:
|
||
|
||
1. Обновление токена по `refresh_token` с сохранением полного набора и понятной ошибкой, когда `refresh_token` отсутствует или отвергнут.
|
||
2. Раздельная проверка срока `access_token` и `id_token` с запасом по времени; в статусе профиля видно, что именно просрочено.
|
||
3. Переключение аккаунта как наблюдаемая последовательность: остановка клиента → запись учётных данных → синхронизация → запуск. Каждый шаг сообщает о себе, чтобы интерфейс (зона A12) показал прогресс.
|
||
4. Сбой на любом шаге не оставляет промежуточного состояния: либо переключено полностью, либо возврат к прежнему.
|
||
|
||
**Тесты:** просроченный `access_token` при живом `refresh_token` обновляется без повторного входа; отсутствие `refresh_token` даёт понятную ошибку, а не молчаливый провал; прерывание на середине не оставляет смешанных учётных данных.
|
||
|
||
## P0-4. Обнаружение моделей Antigravity устроено неверно
|
||
|
||
Служба кэширования из A9 работает, но зонд для Antigravity не возвращает ничего. Проверено:
|
||
|
||
```
|
||
discover_models failed: agy.exe -p x --model __invalid_probe__ ... timed out after 20 seconds
|
||
обнаружено моделей: 0
|
||
```
|
||
|
||
`agy_subprocess.py:122` запускает `agy` с **заведомо неверной моделью** и разбирает текст ошибки, надеясь выудить из неё список. Это отказало: команда просто виснет на 20 секунд.
|
||
|
||
При этом у CLI есть штатная команда. Проверено вручную — `agy models` возвращает четырнадцать настоящих моделей, разделитель табуляция:
|
||
|
||
```
|
||
gemini-3.7-flash-high Gemini 3.7 Flash (High)
|
||
gemini-3.1-pro-high Gemini 3.1 Pro (High)
|
||
claude-sonnet-4-6 Claude Sonnet 4.6 (Thinking)
|
||
gpt-oss-120b-medium GPT-OSS 120B (Medium)
|
||
```
|
||
|
||
**Требуется** заменить зонд на `agy models` с разбором табулированного вывода.
|
||
|
||
Осторожно с таймаутом: **та же команда в одном прогоне отвечает за 40 секунд, а в следующем висит больше двух минут.** Это замерено, не предположение. Жёсткий таймаут обязателен, при его срабатывании прежний кэш сохраняется.
|
||
|
||
Заодно убрать выдуманный запасной список в `antigravity_adapter.py:144`:
|
||
|
||
```python
|
||
return list(profile.preferred_models or ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-thinking"])
|
||
```
|
||
|
||
Моделей `gemini-2.5-*` у провайдера **не существует** — настоящие начинаются с `gemini-3`. Это тот же класс дефекта, из-за которого в конфигурацию владельца попал несуществующий `gemini-3.7-flash`, стоящий сейчас у роли `orchestrator` как `default_model`. Нет обнаруженного списка — возвращать пустой.
|
||
|
||
**Тест:** зонд возвращает непустой список на подготовленном выводе `agy models`; при таймауте прежний кэш не затирается; запасного литерала в адаптере нет.
|
||
|
||
## P1-5. Квоты: `source` не должен опережать данные
|
||
|
||
Мелочь, но она из того же семейства, с которым боремся весь проект:
|
||
|
||
```
|
||
opencode-go:opengo-1 source=provider_api
|
||
Общий 5 часов / Недельный / Месячный: remaining=None
|
||
```
|
||
|
||
`source="provider_api"` заявляет измерение, которого не было — все корзины пусты. Либо `source="baseline"`, когда чисел нет, либо отдельное поле, различающее «провайдер ответил, но лимитов не даёт» и «провайдер не ответил». Причина уже пишется правильно, расходится только `source`.
|
||
|
||
---
|
||
|
||
## Что принято и переделке не подлежит
|
||
|
||
Не откатывать при слиянии:
|
||
|
||
- живые квоты Antigravity (`retrieveUserQuotaSummary` с обновлением токена при 401): шесть аккаунтов владельца отдают разные измеренные числа;
|
||
- правка плагина `2d62d39` и тест `tests/test_plugin_passthrough.py`;
|
||
- `_finish` мастера: `destroy()` выполняется всегда;
|
||
- исправление `is_expired` в `do_test_profile`;
|
||
- миграция конфигурации из A9 и точка входа `model_discovery.py`.
|
||
|
||
## Ограничения
|
||
|
||
- Строгая граница по файлам — см. выше.
|
||
- Никаких чисел, идентификаторов и названий моделей без измерения.
|
||
- Сеть и подпроцессы — не в UI-потоке.
|
||
- Тег `v0.1.1` не создавать.
|
||
|
||
## Критерии приёмки
|
||
|
||
1. Ветка в `origin`. Ни один файл зоны A12 не изменён.
|
||
2. Вызов без достоверно определённой роли уходит вниз, не тратя попыток роутера; отказ цепочки никогда не возвращается как ответ ассистента; правка `2d62d39` сохранена.
|
||
3. Граница между учётными системами Hub и Hermes описана в контракте; варианты связывания профилей поданы с ценой каждого, без односторонней реализации.
|
||
4. Токен Codex обновляется по `refresh_token`; переключение останавливает клиент до подмены учётных данных и не оставляет промежуточного состояния; закрыто тестами.
|
||
5. `agy models` используется как зонд; обнаружение возвращает непустой список; при таймауте кэш сохраняется; выдуманного запасного списка в адаптере нет.
|
||
6. `source` квоты не заявляет измерения там, где чисел нет.
|
||
7. Прогон **в окружении с UI-зависимостями**; число пропусков объяснено. На `main` набор даёт 307 passed, 2 skipped.
|
||
8. `ruff check .` чисто. Релизный гейт: назвать состояние до и после, объяснить расхождение с замером ревьюера; ухудшать нельзя.
|
||
9. Отчёт: `START_HEAD`, `FINAL_HEAD`, `origin/main`, `git status`, точный `X passed / Y skipped / Z failed`.
|
||
|
||
## Порядок сдачи
|
||
Передать точный `FINAL_COMMIT_SHA`. **Сдано только после появления ветки в `origin`.**
|