hermes-hub/agents/inbox/2026-08-23-A9-antigravity-quotas-models-migration.md
Hermes Team 95afdfe179 docs(task): A9 — интеграция с Hermes и обновление токенов Codex
P0-00: Hub подключён к Hermes как middleware llm_execution и срабатывает
на каждом вызове, но Hermes не передаёт role — поэтому всё уходило в роль
по умолчанию, цепочка orchestrator исчерпана, и Hermes получал текст
ошибки вместо ответа модели. Следствие устранено в 2d62d39; в задании —
причина: не претендовать на вызов без достоверной роли, описать границу
между учётными системами Hub и Hermes, подготовить варианты связывания
профилей.

P0-01: Codex сохраняет refresh_token, но функции обновления нет вовсе.
Требуется обновление токена, раздельная проверка access_token и
id_token и переключение аккаунта с остановкой клиента до подмены
учётных данных — по образцу Cockpit Tools, присланному владельцем.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 12:27:43 +07:00

303 lines
32 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.

# Задание A9 (Antigravity): миграция конфигурации, квоты остальных провайдеров, реальные модели
## Дата поступления
2026-08-23
## База
Проверочный HEAD на момент выдачи: **`b625b0f`**. Обязательно обновить локальную копию — см. следующий раздел.
## Ветка
`antigravity/quotas-models-migration`
---
## Перед началом: обновить локальную копию
Вы работаете на другой машине и пушите прямо в git. `main` ушёл далеко вперёд вашей базы: в него влиты и A8, и работа Codex (граф маршрутизации, живые квоты, дефекты живого прогона), и правки ревьюера.
```
cd <каталог репозитория>; git fetch origin --prune; git status
```
Если рабочее дерево чистое:
```
git checkout main; git reset --hard origin/main
```
Зафиксировать фактический `BASE_SHA` через `git rev-parse --short HEAD` и указать его в отчёте. Не считать `b625b0f` актуальным автоматически.
---
## Что принято по A8
Проверено исполнением:
- **Самолечение запуска работает.** `launcher_bootstrap` импортируется, `check_missing_dependencies()` на чистой машине возвращает `[]`, все пять функций на месте.
- **`find_free_slot` больше не выдумывает идентификаторы.** Проверено по всем пяти провайдерам мастера: возвращается либо существующий профиль, либо `None`. Это было главным дефектом A8 и он закрыт.
- **Экран переустановки в мастере есть** и подключён к `SetupEngine.IsInstalled` (`HermesHubSetup.cs:714`), с кнопкой «Переустановить» и показом версий.
- **Зеркальное развёртывание реализовано**: `MirrorDirectoryRecursive` заменил копирование. Проверено на живой машине владельца — при зеркалировании удалились девять устаревших файлов, включая четыре мёртвых модуля, которые мы удаляли из репозитория ещё в прошлых раундах.
- Тесты `test_deployment_doctor.py` проходят, ruff чисто.
Работа хорошая. Но в отчёте три утверждения, которые проверку не прошли, — читайте следующий раздел, это важнее похвалы.
---
## P0-0. Три утверждения отчёта A8, не подтвердившиеся проверкой
Это не придирки к формулировкам. Каждое из трёх означает, что заявленная функция у владельца не работает.
### 1. Профили Claude и Grok до пользователя не дошли
Отчёт: «добавлены по 3 профиля… всего 22 профиля».
Факт на живой машине владельца:
```
профилей в конфиге: 16
antigravity 10
openai-codex 3
opencode-go 3
claude 0
grok 0
find_free_slot(grok) -> None
find_free_slot(claude) -> None
```
Профили добавлены во **встроенные умолчания** (`router_config.py`) и в **пример** (`router_profiles.example.yaml`). Но `load_router_config()` возвращает умолчания **только если файла нет** (`router_config.py:324`). У владельца файл есть — `%LOCALAPPDATA%\hermes\config\router_profiles.yaml`, и он побеждает. Новые встроенные профили в существующую установку не попадают никогда.
Прямое следствие — жалоба владельца **«при подключении грока ошибка»**: мастер получает `None`, показывает «свободный слот не найден», и Grok с Claude подключить невозможно в принципе.
**Требуется миграция конфигурации.** При загрузке существующего `router_profiles.yaml` профили и роли, появившиеся во встроенных умолчаниях позже, должны в него добавляться, а не игнорироваться. Условия:
- пользовательские правки не затираются: если профиль с таким `profile_id` уже есть, он остаётся как есть;
- добавление фиксируется в журнале и видно в самопроверке;
- у файла есть версия схемы, чтобы миграция была идемпотентной и не повторялась;
- перед первой записью делается резервная копия рядом с файлом.
**Тест:** взять конфиг из 16 профилей без claude/grok, выполнить загрузку, убедиться, что после неё `find_free_slot("grok")` возвращает существующий профиль, а десять профилей `antigravity` не изменились ни в одном поле.
### 2. Проверка просроченной авторизации была мертва
Отчёт: «добавлена предварительная проверка `status.get("expired")`».
Ключ в словаре называется **`is_expired`** (`profile_manager.py:414`), поэтому `status.get("expired")` всегда `None`. Проверка не срабатывала ни разу. Ваш собственный тест этого не поймал, потому что дефект прикрывала вторая проверка — в адаптере; когда при слиянии вызов адаптера из «Теста» ушёл, протухший аккаунт стал получать зелёную галочку.
Исправлено ревьюером при слиянии (`b625b0f`), трогать не нужно. Приводится как урок: тест проверял результат, достижимый двумя путями, и молчал о том, что один из них сломан.
### 3. Пересобранный установщик до владельца не доходит
Отчёт: «Перекомпилирован `dist/HermesHubSetup.exe` и обновлен `dist/checksums.txt`».
`dist/` числится в `.gitignore:9`. Через этот репозиторий бинарник не передаётся физически — он остался на вашей машине. Владелец ставит из своей локальной сборки.
**Требуется** описать в отчёте, как собранный установщик должен попадать к владельцу: публикация в `hermes-hub-releases`, снятие `dist/` с игнорирования, или сборка на стороне владельца одной командой. Выберите один способ и обоснуйте. Пока способа нет, утверждать «установщик обновлён» нельзя.
### 4. Флаги `/reinstall` и `/repair` разбираются, но ни на что не влияют
Отчёт: «Поддержан флаг командной строки `/reinstall` (и алиас `/repair`)».
`HermesHubSetup.cs:968``bool isRepair = false;`, присваивается на строке 975 и **больше не используется нигде**. Это видно даже компилятору:
```
HermesHubSetup.cs(968,18): warning CS0219: Переменной "isRepair" присвоено значение,
но оно ни разу не использовалось
```
Запуск с `/reinstall` без `/silent` просто открывает обычный мастер. Либо реализовать тихую переустановку с кодами возврата, либо убрать флаг и не заявлять его.
---
## P0-00. Hub перехватывает КАЖДЫЙ вызов Hermes как `orchestrator`
Это самое важное в задании. Владелец сообщил: «зашёл в Гермеса, а там наш хаб не работает, основной оркестратор не выбрался», и сделал вывод, что Hub к Hermes не привязан. Вывод неверный, а положение хуже: **Hub привязан и активно ломал Hermes.**
Что установлено разбором кода Hermes и его журналов:
1. Плагин регистрируется штатно. `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. **Не претендовать на вызов без роли.** Если роль не определена достоверно, Hub не должен подменять выбор Hermes: пропускать вниз сразу, не тратя попыток. Роль по умолчанию для внешнего перехвата — неверная модель поведения.
3. **Описать честную границу продукта.** Hermes ведёт собственные профили (`agy-01`…`agy-06`, `worker-fast`, `worker-research`, `worker-review`, `worker-code`, `worker-code-2`, `deepseek`) в каталоге `profiles` своего домашнего каталога, и его `delegate_task` настроен отдельно (`max_concurrent_children=3`, `provider=opencode-go`, `model=kimi-k2.7-code`). Профили Hub (`ag-w1`, `codex-orch`, `opengo-*`) — **другое множество идентификаторов**, ничем с ними не связанное. То есть один и тот же аккаунт Google живёт в двух учётных системах под разными именами.
Написать в `docs/UI_STATE_CONTRACT.md` раздел о границе: что Hub видит от Hermes, чего не видит, чем управляет и чем не управляет. Без этого любые обещания интерфейса о «команде агентов» вводят владельца в заблуждение — он видит на экране Hub роли, которые Hermes не спрашивает.
4. **Предложить путь связывания** профилей Hub с профилями Hermes и оценить его трудоёмкость: сопоставление по идентичности аккаунта (email из `id_token`), либо чтение профилей Hermes как источника, либо явная таблица соответствия. Решение принимает владелец — вам подготовить варианты с ценой каждого, не реализовывать молча.
**Тесты:** вызов без определяемой роли уходит вниз, не тратя попыток роутера; вызов с определённой ролью маршрутизируется; отказ цепочки никогда не возвращается как ответ ассистента.
## P0-01. 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
```
Ключевое в этой схеме: **токены проверяются по отдельности**, обновляется весь набор, и **клиент останавливается до подмены учётных данных, а не после**.
Состояние у нас:
- `codex_oauth.py` сохраняет `refresh_token` (строка 226), но **функции обновления не существует**. Для Antigravity есть `oauth.refresh_access_token`, для Codex — ничего. Протухший токен Codex означает полный повторный вход вместо тихого обновления.
- Раздельной проверки `access_token` и `id_token` нет.
- Остановки клиента Codex при смене аккаунта нет вовсе: подмена учётных данных под работающим процессом оставляет его со старыми.
**Требуется:**
1. Обновление токена Codex по `refresh_token`, с сохранением полного набора и понятной ошибкой, когда `refresh_token` отсутствует или отвергнут.
2. Раздельная проверка срока `access_token` и `id_token` с запасом по времени; в статусе профиля видно, что именно просрочено.
3. Переключение аккаунта как последовательность с остановкой клиента **до** записи учётных данных и запуском **после**. Шаги должны быть наблюдаемыми — интерфейс покажет их прогрессом (это часть B8), а от вас нужен backend, который эти шаги выполняет и сообщает о каждом.
4. Сбой на любом шаге не оставляет систему в промежуточном состоянии: либо аккаунт переключён полностью, либо всё вернулось к прежнему.
**Тесты:** просроченный `access_token` при живом `refresh_token` обновляется без повторного входа; отсутствие `refresh_token` даёт понятную ошибку, а не молчаливый провал; прерывание на середине переключения не оставляет смешанных учётных данных.
---
## P0-1. Квоты для OpenAI Codex и OpenCode Go
Жалобы владельца: **«лимиты не подтягиваются, всё стоит Н/Д»** и **«у опенкода тоже нет лимитов»**.
Для Antigravity это уже решено — Codex реализовал живой опрос `retrieveUserQuotaSummary` у Google. Проверено на шести авторизованных аккаунтах владельца, данные настоящие и разные:
```
ag-w2 Claude/GPT — неделя remaining=37.4 used=62.6 source=provider_api
ag-w3 Claude/GPT — неделя remaining=90.1 used= 9.9 source=provider_api
ag-w1 Gemini — неделя remaining=99.7 used= 0.3 source=provider_api
```
Для двух других провайдеров осталась заглушка `_generate_baseline_snapshot` — все поля `None`:
```
opencode-go:opengo-1 source=provider_api
reason = "нет живого ответа от лимитов OpenCode Go"
Общий 5 часов / Недельный / Месячный: remaining=None
```
**Требуется** довести до реальных данных `openai-codex` и `opencode-go` по тому же образцу: опрос настоящего эндпоинта провайдера с использованием сохранённых учётных данных, обновление токена при 401, `source="provider_api"` только когда числа действительно измерены.
Правило честности прежнее и оно важнее полноты: **если провайдер данных не отдаёт — `None` и внятная причина, а не правдоподобное число.** Текущее поведение OpenCode Go в этом смысле правильное, оно просто неполное. Если у провайдера эндпоинта лимитов нет вовсе — это законный результат: зафиксировать в `docs/UI_STATE_CONTRACT.md` как недоступное, с причиной, чтобы интерфейс подписал честно.
**Тест:** на подготовленных учётных данных снапшот содержит измеренные значения; при ответе провайдера 401 — понятная причина и `None`; ни при каком сбое не появляется выдуманное число.
## P0-2. Служба обнаружения моделей: кэш, фон, таймаут
Владелец просит выбор моделей для агентов (жалоба 2). Интерфейс делает Codex, но опора нужна ваша.
`discover_models` есть у всех адаптеров, и для Antigravity он работает: `agy models` вернул 14 настоящих моделей.
Но вызывать его из интерфейса напрямую нельзя. Замерено на живой машине: **тот же `agy models` в одном прогоне отвечает за 40 секунд, а в следующем висит больше двух минут.** Синхронный вызов заморозит окно намертво.
**Требуется** служба обнаружения моделей:
- результат кэшируется на диске рядом с конфигурацией, с временем получения;
- обновление — в фоне, с жёстким таймаутом и понятным поведением при его срабатывании;
- интерфейс получает список мгновенно из кэша плюс признак свежести;
- при пустом кэше отдаётся `None`, а не выдуманный список — интерфейс покажет «список моделей ещё не получен»;
- ошибка обнаружения не должна ронять карточку и не должна молча подставлять умолчания.
**Тест:** обнаружение с искусственной задержкой дольше таймаута не блокирует вызывающий поток и оставляет прежний кэш.
## P0-3. Выдуманные списки моделей
`auto_assigner.ensure_profile_definition` (добавлен Codex, но это ваша зона) подставляет новым профилям жёстко зашитые списки:
```python
"grok": (["grok-3", "grok-3-mini", "grok-2"], ...),
"antigravity": (["gemini-3.7-flash", "claude-sonnet-4-6", "gemini-3.5-flash"], ...),
```
Это тот же класс дефекта, с которым мы боролись в квотах, только про модели. И он уже даёт ложь: у живого провайдера **`gemini-3.7-flash` не существует**. Реальный список:
```
gemini-3.7-flash-high / -medium / -low
gemini-3.6-flash-high / -medium / -low
gemini-3.5-flash-high / -medium / -low
gemini-3.1-pro-high / -low
claude-sonnet-4-6, claude-opus-4-6-thinking, gpt-oss-120b-medium
```
При этом `gemini-3.7-flash` стоит в живом конфиге владельца как `default_model` роли `orchestrator`, а `gemini-3.6-flash-high` у роли `fast` — существует. То есть часть ролей настроена на несуществующую модель.
**Требуется:**
1. Списки моделей для новых профилей брать из обнаружения (P0-2), а не из литерала. Пока обнаружение не выполнено — оставлять список пустым; профиль без списка моделей честнее профиля с выдуманным.
2. Проверка конфигурации: модели, которых нет у провайдера, отмечаются в самопроверке и в контракте как недействительные, с указанием роли и профиля. Молча подставлять «похожую» модель нельзя — это решение владельца.
3. Разобраться, почему вызов с `gemini-3.7-flash` до сих пор не приводил к явной ошибке. Если провайдер молча подставляет ближайшую — это надо знать и написать в отчёте, потому что тогда владелец получает не ту модель, которую выбрал.
**Тест:** профиль, созданный при отсутствии кэша моделей, не содержит ни одного идентификатора модели; проверка конфигурации сообщает о модели, отсутствующей у провайдера.
## P1-4. Жёсткие срезы в данных для диаграммы
`dashboard_view.py:601``providers = list(snapshot.providers)[:3]`. Провайдеров пять, два молча отбрасываются. Отрисовка — зона Codex, и срез уберут там, но решение о том, сколько провайдеров вообще имеет смысл показывать и в каком порядке, принимается на стороне данных: сейчас порядок ничем не задан, поэтому какой именно провайдер исчезнет — дело случая.
Задать явный, устойчивый порядок провайдеров в снапшоте (например, по числу авторизованных профилей, затем по алфавиту) и описать его в контракте.
## P1-5. Остаток по YAML
Внутренние комментарии `router_profiles.yaml` по-прежнему теряются (7 → 2). Пункт висит с A7 и в A8 не закрыт. Либо полный round-trip, либо статус «частично» с перечнем теряемого — в контракте и в отчёте. С учётом P0-0.1 это стало важнее: миграция будет писать в этот файл, и терять при каждой записи комментарии владельца нельзя.
---
## Ограничения
- Граница: зона Codex — `src/antigravity_provider/router/ui/**`, `tests/test_ui_*.py`. По `hermes_hub_app.py` действует прежнее исключение для backend-функций вроде `do_test_profile`, но не для представления.
- Никаких чисел и идентификаторов без измерения. Нет данных — `None` и причина.
- Тег `v0.1.1` не создавать.
- Резервная копия `router_profiles.yaml` перед первой записью миграции — обязательна.
## Критерии приёмки
1. Ни один файл зоны Codex не изменён.
2. Вызов без достоверно определённой роли уходит вниз, не тратя попыток роутера; отказ цепочки никогда не возвращается как ответ ассистента; правка `2d62d39` сохранена.
3. В контракте описана граница между учётными системами Hub и Hermes; подготовлены варианты связывания профилей с оценкой цены каждого.
4. Токен Codex обновляется по `refresh_token`; переключение аккаунта останавливает клиент до подмены учётных данных и не оставляет промежуточного состояния.
5. На существующем конфиге без claude/grok после загрузки `find_free_slot` для обоих возвращает существующий профиль; десять профилей `antigravity` не изменены; проверено тестом.
6. Миграция идемпотентна и не теряет пользовательские правки и комментарии.
7. Квоты `openai-codex` и `opencode-go` приходят измеренными либо `None` с причиной; ни одного выдуманного числа; проверено тестом на обоих исходах.
8. Обнаружение моделей кэшируется, обновляется в фоне и не блокирует вызывающий поток при таймауте; проверено тестом с искусственной задержкой.
9. Новые профили не содержат выдуманных моделей; проверка конфигурации сообщает о несуществующих моделях в ролях владельца.
10. `/reinstall` либо работает с кодами возврата, либо удалён; предупреждение CS0219 при сборке отсутствует.
11. В отчёте назван конкретный способ доставки установщика владельцу.
12. Прогон **в обоих окружениях**; обе команды и оба результата в отчёте.
13. `ruff check .` чисто. Про релизный гейт: он **красный на `main` уже сейчас** (проверка 4 падает не по вашей вине). Указать в отчёте его состояние до и после ваших правок; ухудшать нельзя.
14. Отчёт: `START_HEAD`, `FINAL_HEAD`, `origin/main`, `git status`, точный `X passed / Y skipped / Z failed`.
## Главное
Владелец сказал: «надо чтобы хаб уже заработал». Ядро работает — живой каскад отказоустойчивости в журнале это доказал, и квоты Antigravity теперь настоящие. Осталось, чтобы не работающее выглядело как не работающее, а не как Н/Д без объяснений, и чтобы провайдер, который он хочет подключить, подключался.
## Порядок сдачи
Передать точный `FINAL_COMMIT_SHA`.