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

32 KiB
Raw Permalink Blame History

Задание 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:968bool 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
  1. Роутер возвращал «⚠️ 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-01agy-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, но это ваша зона) подставляет новым профилям жёстко зашитые списки:

"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:601providers = 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.