A48 (Codex): вёрстка по макетам. style.css изменён на 178 строк, добавлен
workspace.js, приложены 79 скриншотов — до, после, макеты для сверки.
Проверено глазами: логотип, шапка с поиском, панель инструментов холста,
карточки узлов с моделью и аккаунтом, читаемые подписи связей, инспектор с
вкладками, три карточки внизу. Выдуманных чисел из макета нет, пустые
состояния честные.
A47: хранилище AI-Memory под git локально, память проекта приведена к
действительности, worklog заполнен, составлен перечень агентов сервера.
A45: три кандидата замерены. Файлы и размеры сверены по диску, контрольная
сумма Qwen3-Coder пересчитана независимо и совпала. Скорость 109,6 ток/с
на 64К независимо НЕ перемерена: свободно 1,9 ГБ видеопамяти, проверка
потребовала бы остановить рабочий кодер владельца.
A42: подключение OpenRouter, NVIDIA и Ollama, отдельные ветки обнаружения
моделей, отказ с причиной вместо мнимого успеха.
Конфликт A42 с A41 разрешён в пользу A41: ветка A42 отведена от e7194d3,
до слияния A41 в main, поэтому в ней не было динамических префиксов.
Сохранены _get_prefix и генерация слотов; зашитый provider_slots не взят.
Расширенный список ролей openrouter из A42 принят.
Ожидание теста A42 поправлено: nvidia и nvidia-nim — псевдонимы одного
провайдера с одним адаптером, слоты у них общие.
517 passed, ruff clean, релизный гейт 10/10 и на конфигурации владельца,
и на пустой.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1. P0-1: В action_handler.py в add_account реализовано реальное сохранение профилей
и учетных данных для openrouter, nvidia, nvidia-nim, ollama, local, claude, opencode-go.
Для некорректных действий возвращается честная ошибка ok: False вместо мнимого успеха.
В AutoAssigner добавлены слоты и возможности для openrouter и nvidia.
2. P0-2: В ModelDiscoveryService убраны зашитые списки PID. Добавлены ветки
openrouter, nvidia и выделенная ветка ollama (/api/tags и /v1/models). Ошибки серверов
сохраняются и передаются в интерфейс.
3. P0-3: В unified_health.py разделены статусы временного отката ошибки (STATUS_COOLDOWN)
и исчерпания квоты (STATUS_QUOTA_EXHAUSTED). RATE_LIMITED проверяется до кулдаунов.
В health_tracker.py исключена пометка всего аккаунта при пустом model_name.
В codex_adapter.py уточнена классификация ошибок.
4. tests/test_a42_provider_connect.py: 15 тестов, 500 passed, ruff чисто.
Экран маршрутизации брал профили из currentSnapshot.profiles, тогда как
/api/snapshot отдаёт dataclasses.asdict(HubSnapshot), где поле называется
all_profiles. Ключа profiles в ответе нет — проверено перечислением полей
датакласса. Это было единственное такое место в файле: остальные девять
обращений уже читали all_profiles.
Следствия, которые чинятся разом:
- правая колонка «доступные аккаунты» была всегда пуста;
- «0 аккаунтов» оставалось литералом из разметки, счётчик не переписывался;
- строки цепочек получали пустой профиль, provider становился 'unknown',
и все аккаунты рисовались одной иконкой-заглушкой.
Для строк цепочек берётся полный список профилей, для колонки «доступные» —
только подключённые (правило A26).
Убрана полоса квоты с зашитым width:80%: она была одинаковой у всех
аккаунтов и ни на чём не основана. Вместо неё индикатор измеренного
состояния; неизвестное состояние остаётся серым, а не выдаёт себя за
здоровое.
Подписи связей на холсте центрируются (text-anchor отсутствовал, поэтому
подпись уходила вправо от середины связи и обрезалась о край холста) и
получают обводку, чтобы читаться поверх линии.
Инспектор агента показывал модели из preferred_models — это настроенный
список предпочтений профиля, а не то, что даёт провайдер; model_states
строится перебором того же preferred_models, поэтому запасная ветка давала
тот же набор. Источником стал discovered_models провайдера. Настроенная у
агента модель теперь всегда присутствует в списке: раньше, если её там не
было, ни один option не получал selected, показывался первый вариант, и
сохранение конфигурации молча подменяло модель агента.
486 passed, ruff clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1. P0-1: get_default_router_config() возвращает чистую конфигурацию (0 профилей,
13 канонических ролей с пустыми цепочками). Миграция не внедряет фиктивные профили.
2. P0-2: Учетные данные (~/.hermes/*_profiles/, hub_settings.json) изолированы и
никогда не затрагиваются при сбросе или установке.
3. P0-3: Профили создаются динамически при подключении аккаунтов (ag-1, codex-1, etc.).
Пустые цепочки ролей являются нормальным рабочим состоянием.
4. P0-4: Добавлен экшен reset_router_config и кнопка «Начать настройку заново»
в настройках с подтверждением и созданием бэкапа router_profiles.yaml.bak_<timestamp>.
5. P0-5: scripts/verify_multi_provider_router.py адаптирован и проходит 10/10 PASS
как на пустой конфигурации, так и на заполненной.
6. tests/test_a41_clean_install.py: 6 тестов, 490 passed, ruff чисто.
Брендбук требует три темы, причём Medium — отдельная, а не осветлённая Dark:
тёмно-зелёный холст #1A2A1F со светлыми кремовыми карточками #F7F1E3.
В style.css она описана целиком и отрисовывается верно — проверено
подстановкой data-theme вручную. Но в список выбора не попала, а applyTheme
знал только light и dark, поэтому любое другое значение сбрасывало тему в
системную. Выбрать Medium было невозможно.
После правки все три переключаются:
dark фон #101510 карточки #1A2A1F
medium фон #1A2A1F карточки #F7F1E3
light фон #F7F1E3 карточки белые
Цвета совпадают с палитрой брендбука. 486 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1. OllamaAdapter: поддержка локального инстанса по умолчанию и удаленного Ollama API
с кастомным base_url и опциональным Bearer токеном, discovery по /v1/models и /api/tags,
статус квоты «Без ограничений».
2. ClaudeAdapter: реальный health_check API probe и динамический discover_models.
3. OpenRouterAdapter: обязательные заголовки HTTP-Referer и X-OpenRouter-Title,
сбор метаданных моделей (context_length, display_name).
4. NvidiaAdapter: парсинг заголовка Retry-After и динамическая задержка при 429.
5. Экспорт лимитов: эндпоинт GET /api/quotas/export (JSON / CSV), экшен export_quotas
и кнопка выгрузки в веб-интерфейсе с маскированием секретов.
6. tests/test_api_providers_a32.py: 15 тестов, 434 passed, ruff чисто.
1. P0-1: Восстановлены все веб-обработчики в app.js (openAddAccountWizard,
handleNodeAccountChange, handleNodeModelChange, handleRefreshProviderModels,
checkUpdates), связаны с потоками startDeviceAuth и startRedirectAuth,
выбор слота обязателен и понятен пользователю.
2. P0-2: Добавлены адаптеры OpenRouter и NVIDIA NIM с поддержкой динамического
base_url, множественных аккаунтов без ограничений, GET /models и честным
отображением квот/«Н/Д».
3. P0-3: В мастере подключения локального провайдера реализована кнопка автопоиска
(discover_local_models), отображение серверов, ошибок портов и автозаполнение.
4. P0-4: Полностью удален устаревший десктопный интерфейс CustomTkinter
(router/ui/** 20 файлов, hermes_hub_app.py), зависимости customtkinter и pillow
убраны из pyproject.toml и инсталляторов, оставлен единый ярлык «Hermes Hub».
5. 418 passed, 1 skipped, 4 deselected, ruff чисто.
Реализована поддержка произвольных параметров запроса (request_options) для
локальных профилей:
1. RouterProfileConfig и ProfileViewModel дополнены полем request_options
с полной поддержкой вложенных словарей и сериализацией в YAML.
2. LocalLLMAdapter подмешивает request_options в тело POST /chat/completions.
Явные поля запроса имеют приоритет, при расхождениях пишется warning.
Изоляция провайдеров сохранена: другие адаптеры не трогают request_options.
3. Неизвестные/некорректные параметры обрабатываются чисто и классифицируются
как INVALID_REQUEST с извлечением текста ошибки сервера.
4. В веб-интерфейсе реализована карточка профиля с редактором JSON параметров,
живой валидацией синтаксиса, предпросмотром тела запроса и проверкой
подключения.
5. Отсутствие зашитых параметров enable_thinking и reasoning_effort в логике.
6. 526 passed, 3 skipped, 4 deselected, ruff чисто.
Проверка A37 исполнением. Защита границы рабочей области работает, обходы
через ../ и ~ в validate_path отсекаются, каталоги учётных данных закрыты.
Но в validate_command нашлась дыра ровно в том месте, ради которого guard и
делался.
Тильда и переменные окружения не раскрывались перед проверкой. Путь
"~/.hermes/agy_profiles" не считался абсолютным, склеивался с каталогом
проекта в путь с буквальным "~" внутри и признавался допустимым. Измерено:
rm -rf ~/.hermes/agy_profiles РАЗРЕШЕНО
rm -rf ~/.ssh РАЗРЕШЕНО
rm -rf $HOME/.hermes РАЗРЕШЕНО
тот же путь абсолютным отказ
тот же путь через validate_path отказ
То есть самый естественный способ написать опасную команду обходил защиту, а
абсолютный путь — нет. После правки все три отклоняются, штатное удаление
внутри проекта по-прежнему разрешено.
Добавлен тест, удерживающий это свойство.
Проверено отдельно, что защита не ломает продукт: страница, app.js, health и
snapshot отдают 200, в снапшоте 13 ролей, секретов в ответе нет, проверка
обновлений работает. 508 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверка кандидата A28–A31 исполнением.
1. Дублирование ролей. RoleRegistry.migrate_legacy_roles написана верно, но
НЕ ВЫЗЫВАЛАСЬ НИОТКУДА — проверено поиском по всему коду. Вместо неё
работала «идемпотентная миграция» в router_config, дописывавшая недостающие
умолчания и не убиравшая старые роли. На конфигурации владельца интерфейс
показывал 19 агентов, причём шесть пар были неотличимы по названию:
orchestrator и manager — оба «Менеджер проекта», reviewer и code-reviewer —
оба «Ревьюер кода». Разложить аккаунты по такому списку невозможно.
Миграция подключена. Внутри неё нашлась вторая ошибка: при обходе одним
проходом пустая каноническая роль затирала цепочку, перенесённую из старой.
У владельца сработало бы именно так — в manager попал бы codex-orch вместо
выставленного им ag-orch-fallback. Старые роли теперь обрабатываются
первыми: в них и лежит настроенный порядок аккаунтов.
Проверено на живой конфигурации владельца: было 19 ролей, стало 13, все
цепочки совпадают с исходными.
2. Сохранённый workflow мигрируется вместе с ролями. Идентификаторы агентов
повторяют идентификаторы ролей, и без переименования рёбра ссылались бы на
исчезнувших агентов — «Ребро ссылается на отсутствующего агента».
3. Поиск локальных серверов моделей. Раньше адрес вводился руками. Теперь
опрашиваются известные порты на петле — Ollama 11434, LM Studio 1234,
llama.cpp 8080-8082, vLLM 8000, Jan, GPT4All, Text Generation WebUI, —
параллельно, девять портов за 1.5 с. Наружу идёт только то, что ответило;
список моделей берётся у сервера. Порт, занятый чужим сервисом, показывается
с причиной, закрытые не показываются вовсе. Действие discover_local_models.
Роль в фикстурах test_workflow_service_a30 переименована в test-developer:
«developer» — псевдоним канонической developer-1, и тест проверял бы работу
псевдонимов вместо механики workflow.
475 passed, 2 skipped; ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
В adapters/antigravity_adapter.py жил модульный _AGY_INVOCATION_LOCK, общий
для ВСЕХ профилей Antigravity. Он брался при каждом вызове, у которого есть
учётные данные, то есть при каждом рабочем. Ветка без мьютекса срабатывала
только у профиля без учётки — у вызова, который и так упадёт.
Измерено на живом адаптере: три параллельных вызова по одной секунде
занимали 3.01 с. При десяти подключённых аккаунтах одновременно работал ровно
один, чем обесценивалась вся мультиаккаунтность — то, ради чего хаб и делался.
Мьютекс охранял пустоту. Он был введён в 50fde5f со словами «guarded global
gemini:antigravity credential swap ... to eliminate concurrent subprocess
race», когда подмена учётных данных была ГЛОБАЛЬНОЙ. С тех пор она стала
попрофильной: agy_subprocess пишет в profile_dir/.gemini/oauth_creds.json, а
HOME, USERPROFILE, HOMEPATH и HOMEDRIVE подменяются на каталог профиля.
Общего состояния между профилями не осталось — проверено поиском глобальных
путей и обращений к keyring, их нет.
После снятия: те же три вызова занимают 1.00 с, и каждый идёт со своим HOME
(ag-w1, ag-w2, ag-w3) — изоляция не пострадала.
Ограничение одновременности остаётся за LeaseManager: он считает лизы по
профилю и настраивается через max_concurrency, в том числе значением 1 для
локальных моделей с --parallel 1.
Добавлен тест, удерживающий это свойство: он падает, если вызовы разных
профилей снова начнут сериализоваться. Существующий тест изоляции учётных
данных проходит без изменений.
Найдено при разборе анализа, который Antigravity провёл на сервере владельца
(hermes-muliacount). 459 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Preflight and updater tests no longer probe 8081/8082 or GitHub.
Hermetic runs fail-fast on non-loopback sockets. GUI helpers are
importable without customtkinter. CI installs the web extra, and
pytest-timeout plus a wall-clock wrapper bound the suite.
CORS был настроен как allow_origins=["*"] вместе с allow_credentials=True.
FastAPI в таком сочетании не отдаёт звёздочку, а ОТРАЖАЕТ присланный Origin
обратно. Проверено запросом к работающему хабу:
Origin: https://evil.example.com
-> HTTP 200
access-control-allow-origin: https://evil.example.com
access-control-allow-credentials: true
Опаснее всего это на localhost. get_auth_token требует токен только при
небlocalhost-привязке, то есть на 127.0.0.1 проверки нет вовсе. Значит любая
открытая рядом веб-страница могла прочитать /api/snapshot со всеми
аккаунтами, почтами и квотами и вызвать /api/action — удалить учётные
данные, переписать маршрутизацию, запустить вход OAuth. Ровно так на обеих
машинах владельца хаб и работает.
Собственному интерфейсу CORS не нужен: он отдаётся тем же сервером.
Межсайтовые запросы запрещены по умолчанию, список разрешённых источников
вынесен в настройку web_api_allowed_origins — он понадобится, когда одна
панель будет смотреть на несколько хабов.
Проверено после правки: заголовков access-control в ответе нет, браузер
такой запрос заблокирует; собственный интерфейс работает, снапшот приходит
(24 профиля, 6 ролей, индикатор «Live API»). 451 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверка A27 исполнением. Основа сделана верно — сравнение по коммитам,
чтение релизов основного репозитория, честный показ отказа проверки. Три
дефекта закрыты.
1. В вебе функция была нерабочей целиком. Веб-сервер передаёт async_runner
всегда, а check_updates в этой ветке отвечал «Проверка обновлений
запущена» без data. Результат до клиента не доходил, кнопка обновления не
могла появиться никогда. Проверка — один HTTP-запрос с таймаутом 10
секунд, поэтому выполняется синхронно и всегда возвращает данные; в фон
уходит только установка.
2. Контрольная сумма пропускалась молча. При недоступном checksums.txt
expected_sha оставался пустым, проверка не выполнялась, и скачанный
установщик запускался. Здесь исполняется загруженный из сети код —
непроверенный файл теперь не запускается вовсе, с внятной причиной.
3. Перезапуска не было, но он обещался. Ни install-linux.sh, ни виндовый
установщик в тихом режиме приложение не поднимают, а сообщение гласило
«Hermes Hub будет перезапущен»: владелец остался бы со старым процессом и
решил, что обновление не сработало. Добавлен schedule_restart —
отсоединённый помощник ждёт освобождения порта, текущий процесс выходит
раньше. Установка теперь дожидается завершения установщика и проверяет код
возврата, вместо того чтобы обещать успех сразу после запуска.
Тест test_action_executor_update_actions требовал, чтобы check_updates уходил
в фон, то есть закреплял дефект как требование — переведён на желаемое
поведение.
Проверено: сумма отсутствует — отказ; сумма не совпала — отказ; совпала —
запуск и перезапуск. Через HTTP приходят installed_commit, latest_commit,
release_tag и время публикации. В интерфейсе видно «Доступно обновление
(a1e1db7)», «Сборка: d7ad3e3» и время последней проверки; при отказе сети —
«Ошибка проверки» с причиной, а не «Актуально». 450 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверка A26 исполнением. Признак подключённости был записан как
p.authenticated === true || (p.health_state && p.health_state !== 'not_configured')
и содержит два дефекта. Поля authenticated в ProfileViewModel нет вовсе —
первая половина условия мертва. Вторая пропускает всё, кроме not_configured,
то есть холодный резерв (health_state "disabled") и непроверенные пустые
слоты.
Измерено на живом снапшоте: из 24 профилей фильтр пропускал 5, при том что
по-настоящему подключён 1. При нуле настоящих аккаунтов страница показывала
три карточки «Холодный резерв» вместо пустого состояния — ровно тот мусор,
который владелец просил убрать.
Тот же предикат используется для выбора аккаунта на «Обзоре», поэтому там
предлагалось назначать роли на пустые слоты: мышление слотами, ради отмены
которого задание и делалось.
Authoritative признак — auth_state: у подключённого AUTHENTICATED, у пустого
слота и у холодного резерва NOT_CONFIGURED. AUTH_REQUIRED и AUTH_EXPIRED
означают подключённый аккаунт, которому нужен повторный вход, — показываем.
Заодно в выборе аккаунта показывается почта, а не имя профиля: жалоба из A24
про «Кодер 1 — назначенный аккаунт Кодер 2» иначе возвращалась.
Проверено в браузере: было 5 «подключённых» из 24, стало 2 — оба с
auth_state AUTHENTICATED; холодный резерв из выбора на «Обзоре» исчез.
442 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец подключил аккаунт, увидел его в «Аккаунтах» — и не нашёл ни в
«Обзоре», ни в «Маршрутизации».
Поведение верное: эти два экрана показывают цепочки ролей, а в умолчаниях в
цепочках участвуют только ag-orch-fallback и ag-w1..w4. Слоты ag-cold-* и
ag-spare-2 не входят никуда, поэтому подключённый в них аккаунт там и не
появится. Но мастер показывал все десять слотов одинаково, как «свободен», и
выбор был вслепую — а результат выглядел как пропажа аккаунта.
Теперь в списке слотов видна роль: «ag-w1 — свободен · coder-primary
(primary)» против «ag-spare-2 — свободен · не участвует в маршрутизации».
Участвующие идут первыми. После подключения слота вне цепочек показывается
пояснение, где его добавить.
Признак участия берётся из состава самих цепочек, а не из assigned_roles:
холодный резерв и ag-spare-2 значатся с ролью "spare", которой среди шести
маршрутизируемых ролей нет, так что проверка по названию роли давала бы
неверный ответ.
Проверено в браузере: шесть слотов показаны с настоящими ролями, четыре — с
пометкой о неучастии; состав совпадает с разбором снапшота по цепочкам.
432 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец подключил первый аккаунт Antigravity на сервере, получил
«Авторизация успешно завершена» — и не увидел его в разделе «Аккаунты».
Учётные данные сохранялись правильно: save_profile_auth и load_profile_auth
симметричны, get_profile_status после записи отдаёт authenticated=True —
проверено исполнением. Не совпадало другое: состояние профилей берётся из
кэша UnifiedHealthService, а фоновый цикл веб-сервера обновляет снапшот с
force_scan=False и кэш не трогает.
Измерено на изолированном каталоге:
до входа ag-w1 -> not_configured
сразу после входа ag-w1 -> not_configured
после refresh(force=False) ag-w1 -> not_configured <- цикл делает это
после refresh(force=True) ag-w1 -> not_tested
То есть аккаунт не появился бы никогда, пока хаб не перезапустят.
Теперь после успешного входа выполняется пересбор с force_scan=True — во всех
четырёх точках завершения: device-flow, ручная вставка адреса, опрос
redirect-потока и код Claude. Ошибка пересбора логируется и сам вход не
роняет.
Функция объявлена на уровне модуля: вложенной она была видна не всем точкам,
и device-flow получал NameError внутри обработки успеха. Мой тест этого не
поймал, потому что проверял только redirect-путь — нашла проверка ruff.
Проверено: not_configured -> not_tested сразу после входа, без ручного
обновления; путь device-flow исполняется без NameError. 432 passed, ruff
чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец открыл хаб по сети и не смог войти в Antigravity: кнопка
«Копировать» не давала ничего, а после подтверждения доступа браузер уходил
на 127.0.0.1:51121 его собственной машины и показывал «страница недоступна».
Два дефекта.
1. navigator.clipboard существует только в защищённом контексте — HTTPS или
localhost. По http://192.168.1.81:5800 его нет вовсе, и три кнопки
копирования не работали. Хуже: промис никто не проверял, поэтому они
показывали «Ссылка скопирована», ничего не скопировав. Подтверждено
измерением на живой странице по сетевому адресу: isSecureContext=false,
navigator.clipboard отсутствует. Добавлен общий помощник с запасным
execCommand('copy') и честным сообщением при неудаче.
2. Адрес возврата вёл в тупик, из которого код ещё надо было выковырять со
страницы ошибки, где он часто обрезан. Теперь, когда хаб открыт не с этой
машины, показывается готовая команда проброса порта возврата: тогда
слушатель хаба принимает возврат сам и вставлять ничего не нужно. Порт и
адрес берутся из ответа сервера и window.location, не зашиты.
Запасным путём принимается и голый код: не только полный адрес. Признак
адреса — "?" или "://", но НЕ слэш: коды Google сами содержат его и
начинаются с "4/0A...". Первая версия условия их отсекала — поймано
тестом. Добавлена проверка правдоподобия, иначе произвольный текст уходил
на обмен и давал невнятную ошибку провайдера вместо подсказки.
Проверено исполнением: голый код, полный адрес и вставка без протокола
принимаются; русский текст, короткая строка и строка с пробелом отвергаются
без обращения к провайдеру. Подсказка о пробросе отрисована на живой
странице, открытой по сетевому адресу. 432 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец открыл хаб по сети и получил пустую панель с повторяющимся тостом
«Ошибка сервера: 401». Кода 401 клиент не знал вовсе: ответ падал в общую
ветку ошибок, тост повторялся на каждом опросе, и ни одной подсказки о том,
что нужен токен и где его взять, не было.
Попытка ввести токен в «Настройках» тоже провалилась: рядом с полем токена
стоит кнопка сохранения настроек СЕРВЕРА, а она сама шлёт save_settings и
требует токен — получался замкнутый круг, 401 на попытке ввести токен от 401.
Теперь 401 перехватывается и в fetchSnapshot, и в executeAction: опрос
останавливается, открывается окно с полем ввода и объяснением, откуда взять
значение. Токен проверяется настоящим запросом до сохранения, поэтому
неверный не попадает в localStorage. После принятия окно закрывается, поле в
настройках заполняется, опрос возобновляется.
Дефект, найденный при проверке в браузере: непустой токен с не-ASCII
символами роняет fetch на TypeError, и обработчик умирает молча, ничего не
показав. Заголовки HTTP переносят только ASCII. Добавлена проверка до
отправки и перехват сетевой ошибки — так бывает, когда вместе с токеном
скопирован текст вокруг.
Проверено в браузере на изолированном экземпляре: окно появляется само,
кириллица даёт внятное сообщение, неверный токен отвергается и не
сохраняется, верный принимается — панель оживает, индикатор Live API,
опрос возобновлён. 432 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Раздел 3 контракта требует secrets.compare_digest, но в коде стояло
x_hub_token != required_token, а compare_digest не встречался в router/
вообще. Обычное сравнение строк выходит на первом несовпавшем символе и даёт
утечку по времени. Проверка стала уместной сейчас, когда хаб собираются
открыть по сети.
Сравнение идёт в БАЙТАХ, а не в строках: compare_digest со строками
запрещает не-ASCII и падает TypeError — токен с кириллицей давал бы 500
вместо честного отказа. Выяснено исполнением, а не чтением документации.
Проверено на живом приложении при web_api_host=0.0.0.0: без токена 401,
неверный 401, отличающийся одним символом 401, верный 200. Страница отдаётся
без токена (иначе его негде было бы ввести), в /api/settings токен не
попадает, в снапшоте нет access_token/refresh_token/client_secret.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Если /api/snapshot не отвечал, клиент молча загружал snapshot.example.json —
63 профиля с почтами user@example.test — и ставил индикатор источника в
зелёное через setSourceIndicator(true, ...). На экране появлялась полная
здоровая панель, не имеющая отношения к этому серверу.
Дефект становится опасным ровно в том сценарии, ради которого делался вход с
другой машины: при работе через SSH-туннель достаточно промахнуться портом
или потерять туннель, чтобы принять пример за собственные данные и,
например, решать по нему, у какого аккаунта кончилась квота.
Осознанная работа с фикстурой сохранена: ?fixture=1 и открытие страницы
файлом. Подстановка вместо ответа сервера убрана — отсутствие данных теперь
читается как отсутствие данных.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Мастер подключения писал, что на сервере без экрана вход «через веб-интерфейс
невозможен», и отправлял в консоль по SSH либо переносить каталог
agy_profiles руками. GET /api/health отдавал для этих провайдеров жёстко
вписанное supported: false.
Утверждение оказалось ложным. В коде уже были
ProfileOAuthSession.handle_manual_callback_url и
ClaudeOAuthSession.handle_auth_code — оба принимают вставленное вручную
значение и доводят обмен кода на токены. Наружу их просто не вывели. Браузер
нужен где угодно, а не на машине с Hub: владелец открывает ссылку у себя и
возвращает адрес из адресной строки.
Добавлены действия start_redirect_auth, submit_redirect_callback,
poll_redirect_auth, cancel_redirect_auth. auth_flows теперь отражает
настоящие возможности, а не литерал. Обе заглушки в мастере заменены живым
потоком; мёртвая ветка Claude с полем API Key удалена.
Три дефекта, найденных при проверке исполнением:
1. Одна опечатка при вставке убивала сессию: handle_callback ставил
status="failed" при отсутствии кода или чужом state, и вход приходилось
начинать заново, хотя ссылка оставалась годной. Для ручного ввода такие
ошибки больше не конечные; отказ провайдера конечен по-прежнему.
2. Окно слушателя в 5 минут рассчитано на браузер той же машины. При входе с
другого ПК его не хватает: 20 минут — значение, проверенное на практике.
3. find_free_slot всегда возвращал ag-orch-fallback: занятость определяется по
файлу учётных данных, а agy на Windows держит их в keyring, поэтому все
десять слотов выглядят свободными. Вход затёр бы работающий аккаунт. Слот
теперь выбирает владелец из списка, построенного по снапшоту, с пометкой,
какие заняты и кем.
Два теста закрепляли снятую заглушку: test_headless_server_auth_matrix требовал
слов «Headless» и «agy» в интерфейсе, test_c_state_mismatch требовал
status == "failed". Первый переведён на проверку настоящего потока, второй
усилен: свойство безопасности (отказ без обмена кода) проверяется по-прежнему,
и дополнительно проверено, что после промаха верная вставка доходит до обмена.
Проверено вживую в браузере: список из 10 слотов с пометкой занятости,
выбранный слот доходит до сервера, ссылка настоящая от accounts.google.com,
поле вставки на месте. 431 passed, 2 skipped; ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец сообщил, что в маршрутизации не переставляются блоки, нигде нет
выбора аккаунта для роли и на «Обзоре» ничего нельзя изменить. На скриншоте
при этом видна кнопка «Изменить цепочку», которой в коде уже нет: A24 её
удалил вместе с renderTeam и renderProviders.
Причина не в логике. FileResponse отдавал app.js и style.css без заголовка
Cache-Control, поэтому браузер применял эвристическое кэширование и держал
скрипт от прошлой сборки. index.html при навигации перепроверялся и был
свежим — отсюда смесь нового текста подсказки со старыми кнопками, а
перетаскивание и выбор модели просто отсутствовали в загруженном коде.
Действие reorder_chain при этом исправно: проверено вызовом, порядок
цепочки меняется и сохраняется в router_profiles.yaml.
Заодно подключённые аккаунты выводятся первыми, а пустые слоты
(«не подключён») — в конце группы: рабочие карточки были разбросаны
между пустыми и их приходилось выискивать.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец открыл карточку Grok и увидел «Grok 2h — Н/Д» и статус «Не
проверялся» — хотя сервер в этот момент уже отдавал настоящие данные:
подписка 14%, GrokChat 13%, GrokBuild 1%.
Проверено на живом сервере: и /api/snapshot, и quota_snapshot внутри
профиля содержали правильные корзины. Дефект целиком в клиенте — окно
рисовалось ОДИН РАЗ при открытии и на опрос не реагировало. Открытое до
завершения прогрева квот, оно навсегда оставалось с заглушкой
_generate_baseline_snapshot, а после успешной проверки подключения
по-прежнему показывало «Не проверялся».
Теперь открытый профиль отслеживается и перерисовывается при каждом
применении снапшота. Объявление переменной поднято выше первого
использования: let не поднимается, и обращение раньше объявления
роняло бы обработчик снапшота целиком.
При закрытии окна сбрасывается отслеживание и останавливается опрос кода
устройства — раньше он продолжал работать после закрытия мастера.
Тесты: 431 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец был прав: «апи не нужен». Подтверждено на его аккаунте GROK PRO —
api.x.ai/v1/chat/completions вернул 200 и ответ модели grok-4.3 БЕЗ
покупки кредитов. Прежний 402 был целиком из-за того, что подключён был
другой аккаунт, без подписки. Моё утверждение про «разные кошельки»
окончательно снято.
1. Квота показывала «Н/Д» при живой подписке. Читались только
prepaidBalance и onDemandCap — у подписчика оба нулевые. А расход
подписки лежит в creditUsagePercent, и разбивка по продуктам в
productUsage. Теперь оттуда и берётся: на живом аккаунте выходит
14% за неделю, GrokChat 13%, GrokBuild 1% — ровно то, что владелец
видит на grok.com. Корзина кредитов создаётся только когда они реально
заведены: нули у подписчика — норма, а не повод рисовать пустое.
2. Выбор модели отвергал настоящие имена: «кэш моделей для grok пуст, а
модель grok-4.5 не найдена». Причина — в _probe_provider грока не было
вовсе, обнаружение знало только antigravity, codex, opencode и local.
При этом api.x.ai/v1/models принимает тот же OAuth-токен и отдаёт 12
моделей. Зонд добавлен; grok-4.5 теперь принимается, выдуманная
grok-99-turbo — отклоняется.
Тесты: 428 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец: «удалить так и не могу ненужный». Backend был починен в
d755a07, но кнопка по-прежнему не работала.
Причина в клиенте: он отправлял только profile_id, без provider. Сервер
не знал, в каком каталоге искать auth.json, строил неверный путь и снова
отвечал «удалять нечего».
Идентификатор профиля однозначен, поэтому провайдер теперь берётся из
конфигурации, когда его не передали. Действие работает независимо от
того, кто его вызвал.
Заодно в клиенте: подтверждение перед необратимым удалением и обновление
экрана после — раньше карточка оставалась в прежнем виде, и было
непонятно, сработало ли.
Проверено через веб-API без параметра provider: до — авторизован,
после — нет.
Тесты: 426 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец прислал анонс x.ai/news/grok-hermes: доступ к Grok даётся по
подписке SuperGrok через OAuth, покупать кредиты API не требуется. Моя
формулировка в интерфейсе утверждала обратное — что подписка кредиты не
пополняет и это отдельный кошелёк. Это неверно, и оно уже попадало
пользователю на экран.
Установлено проверками:
- cli-chat-proxy.grok.com/v1/models принимает наш OAuth-токен -> 200,
отдаёт grok-4.6;
- тот же прокси /chat/completions -> 426, требует версию Grok CLI
не ниже 0.1.202, и она читается не из заголовков, которые я перебрал;
- api.x.ai/v1/responses (путь из документации Hermes) -> 402
personal-team-blocked:spending-limit.
При этом подключён ochenstarik@gmail.com, а подписка SuperGrok — на
victor.trushenko@gmail.com. То есть отказ объясняется аккаунтом, а не
природой подписки, и утверждать иное я не мог.
Сообщение переписано на проверяемое: у аккаунта нет ни баланса, ни
лимита трат, а доступ даёт подписка на ЭТОМ же аккаунте либо купленные
кредиты. Само чтение биллинга остаётся — оно работает и показывает
настоящие числа.
Тесты: 422 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец спросил, где кончились лимиты, и показал экран: еженедельный
лимит SuperGrok израсходован на 14%, запас есть. А вызовы через Hub
падали с 402 «out of credits».
Разгадка — два разных кошелька. Подписка SuperGrok покрывает чат
grok.com, а программный доступ списывается с кредитов API. Проверено на
живом аккаунте: prepaidBalance 0, onDemandCap 0. Именно поэтому 402, и
подписка тут не помогает.
Адрес биллинга взят из плагина hermes-grok-usage, который владелец уже
использует: cli-chat-proxy.grok.com/v1/billing принимает ТОТ ЖЕ
OAuth-токен, что и авторизация. Проверено запросом — 200 и данные.
_collect_grok_quota раньше строила пустые корзины и никуда не ходила,
поэтому Hub показывал «Н/Д» и объяснить ничего не мог. Теперь читает
предоплаченный баланс и лимит по мере использования, а при нулевом
балансе пишет причину прямым текстом, включая различие кошельков.
Процент считается только когда есть от чего считать: при нулевом лимите
доля не определена, и подставлять ноль процентов нельзя — показываются
абсолютные значения.
Тесты: 422 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец подключил не тот аккаунт Grok и не смог его убрать: «кнопка не
функционирует». Хуже: действие возвращало ok=True с сообщением об успехе,
а профиль оставался авторизованным.
Причина: сигнатура get_profile_dir — (profile_id, provider), а
do_delete_credentials звала её наоборот. Внутри функции есть костыль,
молча исправляющий перестановку, но только для трёх провайдеров:
antigravity, openai-codex, opencode-go. Для grok, claude и local путь
получался неверным (grok_profiles/grok вместо grok_profiles/grok-worker-1),
файл «не находился», и срабатывала ветка «учетные данные отсутствовали»
с ok=True.
То есть удаление работало у трёх провайдеров из шести, а у остальных
молча лгало.
Теперь используется get_profile_auth_path, который берёт аргументы в
правильном порядке. Отсутствие файла больше не считается успехом
удаления: возвращается честный отказ «удалять нечего».
Проверено на живом профиле: до — авторизован, после удаления — нет,
повторная попытка сообщает, что удалять нечего. Тест покрывает все
четыре провайдера, у которых костыль не срабатывал.
Тесты: 419 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец упёрся в заглушку «подключение через веб-интерфейс пока не
реализовано» и не смог подключить Grok. Backend был готов давно —
start_grok_oauth и start_codex_oauth возвращают настоящие адрес и код, —
но наружу не выведен: подключить эти провайдеры можно было только из
десктопа.
Добавлены действия start_device_auth и poll_device_auth. Адрес и код
выдаёт ПРОВАЙДЕР, интерфейс их только отображает — никаких подставленных
значений, как было с выдуманными GRK-7842 и CDX-9104.
Клиент показывает ссылку с кнопками «Открыть» и «Копировать», код
крупно и моноширинным, и опрашивает состояние каждые три секунды. Отказ
и просроченный код показываются как окончательные, опрос прекращается —
это работает вместе с правкой 50e4de5, научившей опрос различать
authorization_pending, access_denied и expired_token.
Проверено через веб-API: start отдаёт настоящий адрес accounts.x.ai и
код, poll возвращает pending, несуществующая сессия — честный отказ.
Контракт поднят до 1.4, действий стало двадцать одно.
Это снимает главное препятствие к удалению десктопа: он был
единственным путём подключить Grok и Codex.
Тесты: 414 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец: «не даёт зайти в грок через аутх». Backend при этом исправен —
проверено: провайдер возвращает настоящую сессию, адрес
accounts.x.ai/oauth2/device и код.
Дефект в цикле опроса: коды 400, 403 и 404 скопом считались
«авторизация ещё не подтверждена» и опрос молча продолжался. Но в
device-flow сервер сообщает РАЗНЫЕ вещи одним кодом 400, различая их
полем error в теле: authorization_pending, slow_down, access_denied,
expired_token.
Следствие: отказ пользователя и просроченный код выглядели как ожидание.
Мастер показывал «Ожидание подтверждения...» до самого таймаута и не
говорил, что подтверждение уже отклонено или код давно истёк.
Теперь каждый исход обрабатывается по существу: ожидание продолжает
опрос, slow_down увеличивает интервал, отказ и просрочка прекращают его
с внятным сообщением.
Тесты: 414 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Найдено при проверке Grok на живых данных владельца. Вызов падал с
AttributeError: 'str' object has no attribute 'get' — grok_adapter.py:103.
Поле error провайдеры отдают то объектом {"message": ...}, то строкой.
Код безусловно звал .get у результата, и на строковой форме ОБРАБОТЧИК
ОШИБОК ПАДАЛ САМ: сбой происходил ровно там, где обрабатывался другой
сбой, маршрутизация обрывалась вместо перехода к резерву, а настоящая
причина терялась.
После правки причина видна: Grok API Error (403): The OAuth2 access token
could not be validated. То есть у Grok просто протух токен, а выглядело
как поломка кода.
Та же конструкция стояла ещё в пяти адаптерах: claude, codex, opencode,
deepseek, local. Разбор вынесен в base_adapter.extract_api_error_message,
все шесть переведены на него.
Тест проверяет обе формы ответа и отдельно следит, чтобы копии хрупкой
конструкции не вернулись.
Тесты: 411 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Обе работы приняты, проверено исполнением.
A24: ровно семь разделов, renderTeam и renderProviders удалены, кнопки
«Изменить цепочку» нет, перетаскивание блоков есть. Перестановка цепочки
проверена вживую: сохраняется в router_profiles.yaml и откатывается.
A25: провайдер local подключён к настоящему серверу владельца через
SSH-туннель к 127.0.0.1:8081. health_check проходит, /v1/models отдаёт
модель, реальный вызов возвращает «ОК» за 3.1 с. Профили local-1 и
local-2 добавлены. Квота отдаётся отдельным состоянием
(source=local_provider, «Без ограничений»), а не как отсутствие данных.
Адрес сервера нигде не зашит.
Разрешение конфликта в action_handler: A25 внёс edit_route и assign_role
в список «просто навигация», где они возвращают заглушку. В A24 это
работающие обработчики — сохранение цепочки и назначение роли. Приняв
версию A25 целиком, мы бы молча сломали перестановку блоков. Оставлены
оба: локальный провайдер в add_account и рабочие обработчики ниже.
Исправлено при слиянии:
1. Адаптер отдавал пустой ответ как успех. У сервера владельца
--reasoning on --reasoning-budget 4096: при скромном max_tokens весь
бюджет уходит на рассуждения, llama.cpp возвращает 200, заполняет
reasoning_content и оставляет content пустым. Проверено на живой
модели: max_tokens=40 — ответа нет, 200 — приходит «ОК». Роутер
засчитал бы такой вызов, а пользователь не получил бы ничего.
Теперь это явный отказ с объяснением, и срабатывает переключение.
Проверка вынесена из блока перехвата: иначе оборачивалась в
«Transport Error», хотя транспорт отработал штатно.
2. Заглушка в тесте A25 возвращала "choices": [] — такого настоящий
сервер не отдаёт. Приведена к реальному виду.
Тесты: 404 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец: «грок не даёт добавить https://x.ai/device» — страница отдаёт
404. Причина хуже опечатки в адресе.
Веб-мастер для Grok и Codex не был подключён к серверу ВООБЩЕ. Он
показывал жёстко вписанный адрес x.ai/device (настоящий —
auth.x.ai/device, и тот приходит от провайдера полем verification_uri) и
ВЫДУМАННЫЕ коды устройства GRK-7842 и CDX-9104. Пользователь вводил бы
несуществующий код бесконечно.
Это тот самый класс дефекта, который вычищали из десктопа в первом
аудите, вернувшийся в новом коде.
Выдуманные значения убраны. Пока поток не проведён через веб-API, шаг
честно сообщает, что подключение через веб не реализовано, и указывает
рабочий путь — десктопное приложение, где поток проведён полностью.
Отдельно: тест test_headless_server_auth_matrix ТРЕБОВАЛ наличия
"https://x.ai/device" в коде, то есть закреплял дефект как требование.
Переписан на противоположное — запрещает выдуманные коды и зашитые
адреса провайдеров.
Тесты: 375 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Со скриншота владельца: заголовок «Ролей в строю: 0/6», а ниже шесть
предупреждений «роль работает через резервный аккаунт». Пять ролей
исправно отвечали, интерфейс сообщал, что не работает ни одна.
roles_ready считал только роли со здоровым ОСНОВНЫМ профилем. Роль,
обслуживаемая резервом, попадала в degraded_roles, но не в ready.
Ошибка в худшую сторону: отказ показывался там, где всё работает — а
переключение на резерв это ровно то, ради чего продукт и создан.
Теперь роль с живым резервом считается работающей и одновременно
помечается деградировавшей: оба состояния остаются различимыми.
Попутно склонение: «Есть 1 ролей без рабочего маршрута» заменено на
согласованное с числом — 1 роль, 2 роли, 5 ролей.
Проверено на живых данных: 6/6 в строю, состояние «деградация», а не
«критическое».
Тесты: 375 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Три дефекта из установки владельца на вторую машину.
1. Веб-сервер падал сразу: «web server process terminated unexpectedly».
Установщик ставит в venv Hermes только customtkinter, pillow, pyyaml и
psutil — fastapi и uvicorn отсутствовали в списке вовсе. Добавлены во
все шесть мест: проверка, сообщение, pip, uv, перепроверка.
Поэтому на Windows открывался только десктоп: веб физически не мог
стартовать.
2. Лаунчер показывал голое «terminated unexpectedly» без причины — та же
болезнь, что у кода 12. Теперь перехватывает вывод процесса и выводит
последние строки ошибки в окне.
3. Окно тормозило при перетаскивании и продолжало двигаться несколько
секунд после отпускания мыши. _RouteDiagram перерисовывал всю канву на
КАЖДОЕ событие <Configure>, а при перетаскивании их сотни; очередь не
успевала разгребаться. Гашение в приложении существовало, но
_handle_debounced_resize был пустой заглушкой.
Перерисовка сведена к одной после затишья. Замерено: 199 событий
давали 199 перерисовок, теперь 4.
Тесты: 373 passed, ruff чисто. Установщик пересобран.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Найдено проверкой всех шести ролей на живой машине владельца. До правок
работали три роли из шести.
1. antigravity_adapter.classify_error возвращал ErrorCategory.UNKNOWN —
значения с таким именем не существует, есть AUTH_REQUIRED, FATAL,
INVALID_REQUEST, QUOTA_EXHAUSTED, RATE_LIMITED, TRANSIENT. Обращение
роняло сам классификатор с AttributeError, то есть отказ происходил
ровно там, где обрабатывался другой отказ, и маршрутизация обрывалась
вместо перехода к резервному профилю. Заменено на FATAL по образцу
codex и opencode: неразобранная ошибка не должна давать повторов.
2. Уровень усилия не подставлялся, если у конкретного профиля не выполнен
вход agy: карта усилий строится обнаружением ЧЕРЕЗ этот профиль, и при
неудаче оставалась пустой. Уровни же — свойство модели, а не аккаунта.
Добавлен запасной источник: сохранённый на диске список моделей со
склеенными идентификаторами вида gemini-3.7-flash-high, из которых
уровни выводятся напрямую и переживают перезапуск.
После правок работают все шесть ролей, включая живое переключение
'fast': opengo-1 -> ag-w4.
Закрыто тестами, включая защиту от возврата несуществующей категории.
Тесты: 373 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверено исполнением, оба главных пункта закрыты.
P0-1, восстановление после отказа авторизации: выбран самый честный из
предложенных вариантов — снятие отметки по событию починки, а не по
таймеру. HealthTracker подписывается на EVENT_ACCOUNT_ADDED и
EVENT_ACCOUNT_AUTH_CHANGED. Проверено: профиль с AUTH_REQUIRED после
события возвращается в строй без ручного вмешательства.
P0-2, проверка моделей. Выдуманная модель отклоняется; базовое имя
gemini-3.7-flash принимается (поправка учтена); склеенное
gemini-3.1-pro-high тоже. Главное — при ПУСТОМ кэше выдумка больше не
проходит: молчаливое согласие устранено.
P0-3, ручное обновление списка моделей: действие refresh_models плюс
кнопка в клиенте.
Исправлено при слиянии:
1. Блокировка файла состояния была переведена с неблокирующей на
БЕСКОНЕЧНО блокирующую. Исходный вариант был неверен — при неудаче
исключение проглатывалось и запись шла без блокировки, — но
бесконечное ожидание хуже: на Unix flock(LOCK_EX) висит вечно, и один
застрявший держатель подвесил бы приложение целиком. Впереди
Linux-сервер. Ожидание ограничено 5 секундами, дальше честный отказ.
Проверено: 6 потоков по 5 записей — 0.11 с, ошибок нет.
2. Действие refresh_models не было занесено в контракт. Ровно тот дрейф,
ради предотвращения которого контракт и существует. Контракт поднят
до 1.3, действий стало девятнадцать.
Тесты: 370 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец возразил на моё утверждение, что gemini-3.7-flash не существует.
Он прав, утверждение было неверным, и я повторил его в четырёх заданиях.
gemini-3.7-flash — настоящее семейство, уровень усилия у неё отдельный
параметр. В интерфейсе Antigravity это видно прямо: пункт «Gemini 3.7
Flash» с вложенным выбором Low/Medium/High. В коде это отражено:
_display_to_cli разбирает «Gemini 3.7 Flash (High)» в пару
("gemini-3.7-flash", "high"). Меня ввела в заблуждение первая колонка
вывода agy models со склеенными идентификаторами.
Настоящий дефект был в коде: _model_supported_efforts вызывала
discover_models() БЕЗ профиля, то есть в глобальном окружении без входа.
Карта поддерживаемых усилий оставалась пустой, подстановка уровня по
умолчанию не срабатывала, и agy отвергал вызов с «requires --effort» —
при совершенно настоящей модели.
profile_id проведён через agy_generate в _model_supported_efforts.
Проверено исполнением: gemini-3.7-flash без указания усилия отрабатывает
и возвращает ответ.
Моки в test_antigravity_concurrency приведены к терпимости по kwargs.
Добавлена поправка agents/inbox/2026-08-24-CORRECTION-gemini-model-names.md:
ложное утверждение попало в A9, A11, A18 и B8, и без опровержения кто-то
чинил бы несуществующую проблему или сломал рабочую конфигурацию.
Тесты: 359 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Четыре недостающих экрана добавлены: Аналитика, Состояние, Журнал
событий, Настройки. Веб покрывает все девять разделов десктопа.
Два новых эндпоинта реализованы: GET /api/events и GET /api/settings.
Секреты не утекают — проверено исполнением, наружу отдаётся только
признак web_api_token_configured (bool), сам токен отсутствует.
Слияние без конфликтов. Тесты: 359 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверено исполнением, все три требуемых доказательства получены.
1. agy models через профиль Hub: обнаружено 14 моделей, включая
gemini-3.1-pro-high и gemini-3.1-pro-low.
2. Реальный вызов adapter.invoke(ag-w1): модель ответила «ОК».
3. route_request(coder-primary): переключение с отказавшего
codex-worker-1 на ag-w1, ответ получен, router_error отсутствует.
Подход из A20 (синтез oauth_creds.json) действительно был тупиковым;
родной вход agy с подменой HOME решает задачу. Реализация аккуратная:
видимая консоль на Windows через CREATE_NEW_CONSOLE, терминалы на Linux,
HOMEDRIVE выставляется корректно.
P0-4 подтверждён: пересохранение профиля не изменяет ни одного файла в
глобальном ~/.gemini.
Исправлено при слиянии:
1. Инструкция в веб-клиенте вела на НЕСУЩЕСТВУЮЩИЙ файл
launcher/main.py. Заменена на реальный вызов launch_native_agy_login
с указанием профиля.
2. test_headless_server_auth_matrix проверял дословную формулировку и
падал при её правке, хотя поведение оставалось верным. Приведён к
проверке сути, добавлена защита от возврата несуществующего пути.
Конфигурация владельца: обоим кодерам поставлена gemini-3.1-pro-high по
его прямой просьбе — теперь это возможно.
Тесты: 355 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Add openid to OAuth scopes for id_token issuance
- Preserve id_token, scope, and token_type on token exchange and refresh
- Atomically write and sync .gemini/oauth_creds.json in profile directories
- Auto-resolve active profile environment in discover_models
- Cache discovered models in models_cache.json with graceful timeout handling
- Add unit test coverage for full OAuth lifecycle and model discovery caching
A17 принято, проверено на живых профилях владельца: «Работает» больше не
ставится непроверенному профилю. Из 22 профилей теперь 1 «Работает»
(есть записанный успех), 7 «Не проверялся», 11 «Аккаунт не добавлен»,
3 «Отключён». Grok и opengo-1, на которые жаловался владелец, показаны
честно. Добавлено поле last_success_at.
A18 принято частично: действие set_model существует и валидирует модель
по списку провайдера. Но обнаружение моделей (P0-2) не сделано вовсе —
model_discovery_service и зонд не менялись, ручного обновления нет,
кэша на диске нет. Из-за этого set_model отклоняет ЛЮБУЮ модель, включая
настоящую: список провайдера пуст, и валидация не с чем сравнивать.
Правка при слиянии: discover_models запускался в ГЛОБАЛЬНОМ окружении,
где вход agy не выполнен, — при шести рабочих OAuth-профилях. Теперь
принимает profile_id и подменяет HOME/USERPROFILE на каталог профиля,
как это делает adapter.invoke. Таймаут поднят с 10 до 60 секунд.
Это не вылечило симптом, и причина оказалась глубже — см. отчёт.
Тесты: 336 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец: «стоит опенкод аккаунт, который не подключен, у него кончились
лимиты и аккаунт не работает». Лимиты ни при чём.
Мастер подключения сохраняет ключ через ProfileAuthManager, а
_resolve_api_key смотрел только в auth_config из YAML и в переменные
окружения. Хранилище профилей он не читал вовсе — в отличие от grok,
claude и codex, где такая проверка есть.
Следствие: любой аккаунт OpenCode Go, подключённый через интерфейс, был
нерабочим. Маршрутизация падала с «No API key found for OpenCode Go
profile», и эта строка уже попадалась в следе отказов оркестратора.
Проверено на живом профиле владельца: до правки health_check=False и
тест профиля возвращал «локальный runtime недоступен», хотя api_key
лежал в auth.json. После — health_check=True, тест проходит.
Закрыто тестами, включая проверку, что пустое хранилище по-прежнему
даёт отказ, а не ложноположительный результат.
Тесты: 331 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец запустил веб, увидел «Н/Д» у всех аккаунтов и сообщил, что
лимиты не подтягиваются. Через пятнадцать секунд всё появилось: опрос
провайдера просто ещё шёл.
Признак is_loading сервер отдавал (баз�овый снапшот выставляет его при
незавершённом опросе), но клиент его игнорировал и рисовал «Н/Д» — тот
же текст, что у подключённого аккаунта без лимитов. Два разных состояния
выглядели одинаково, и различить их было нельзя.
Теперь во время опроса ячейка показывает «Загрузка…» и «Опрашиваем
провайдера…» вместо прочерка.
Причина отказа важнее флага: если провайдер уже ответил «лимитов не
даю», состояние загрузки подавляется — иначе opencode-go и grok
показывали бы «Загрузка…» бесконечно.
Закреплено тестом в test_web_client_contract.py, включая проверку этого
подавления.
Тесты: 328 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
/api/snapshot отдавал квоты с source="baseline" и нулём измеренных
корзин всегда. Две независимые причины, и лечение одной из них ничего
не давало.
1. state_store наполняет квоты через quota_service.get_snapshot, который
читает кэш и при промахе отдаёт пустую заглушку, живой опрос НЕ
запуская. Кэш никто не грел: в десктопе это делал
_refresh_quotas_on_startup, в вебе аналога не было. Штатный
планировщик службы не спасает — его цикл сначала спит интервал
(300 с по умолчанию) и только потом опрашивает.
2. HubStateStore.get_snapshot() возвращает КЭШИРОВАННЫЙ снапшот и
пересобирает его только при первом вызове. Даже после прогрева квот
ответ оставался прежним. В десктопе пересбор делал _refresh_data.
Добавлен фоновый цикл: прогрев квот при старте, затем пересбор снапшота
каждые 30 секунд. Порядок важен — снапшот, собранный до прогрева,
зафиксировал бы пустые корзины.
Проверено исполнением: квоты появляются через ~10 секунд после старта,
24 измеренных корзины, source=provider_api, ag-w2 Gemini неделя 80.5% —
совпадает с прямым опросом провайдера.
Регрессия закрыта tests/test_web_snapshot_freshness.py, включая проверку
порядка «прогрев перед пересбором».
Тесты: 327 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A15 и A16 сошлись в пустоту: API отвечал, файлы клиента лежали в
репозитории, но server.py не монтировал static — в браузере был 404 и до
интерфейса было не добраться.
Причина организационная и она на ревьюере: контракт описал каталог
static/ в структуре пакета, но в разделе об эндпоинтах не назвал, кто его
отдаёт. Обе стороны выполнили написанное и всё равно не собрались.
Подключены StaticFiles и корневой маршрут. Проверено исполнением:
/ -> 200 (13.5 КБ), /app.js -> 200 (51 КБ), /style.css -> 200 (21 КБ),
/api/health -> 200, /api/snapshot -> 200 (98 КБ).
Тесты: 325 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Правки при приёмке A15. Веб-API и вынесение действий приняты, но
в сданном виде не работали ни то, ни другое.
1. Десктоп был уничтожен. При выносе действий из hermes_hub_app.py
пропало объявление class HermesHubApp вместе с 13 методами каркаса:
__init__, _build_layout, _create_view, _show_view, _refresh_data и
другими. Оставшиеся 14 методов оказались вложены внутрь функции
_load_saved_theme после её return — синтаксически валидный
недостижимый код, поэтому модуль импортировался и дефект выглядел
безобидно. launch_hub() при этом падал бы с NameError.
hermes_hub_app.py восстановлен из main; задание прямо требовало
десктоп не ломать.
2. Дублирование убрано правильным способом: десктоп импортирует пять
do_* из action_handler, второй реализации в проекте нет.
3. Веб-API падал с 500 на обоих значимых эндпоинтах: get_auth_token и
run_server читали config.hub, а такого атрибута у RouterConfig нет.
Настройки живут в hub_settings.json. Работал только /api/health, у
которого нет проверки авторизации, — из-за чего сервер и выглядел
поднявшимся.
4. do_save_settings при переносе потеряла атомарную запись через
os.replace, ensure_ascii=False и вызов set_refresh_interval, то есть
интервал обновления квот из настроек перестал применяться.
Восстановлено.
5. Импорт адаптера был убран внутрь do_test_profile, что делало функцию
неподменяемой в тестах. Поднят на уровень модуля.
6. Версия в /api/health была зашита как "1.0.0" вместо настоящей.
Проверено исполнением: /api/snapshot отдаёт 200 и 12 ключей, полностью
совпадающих с docs/web-api/snapshot.example.json; секретов в ответе нет;
неизвестное действие даёт 404. Тесты: 319 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Работа A15 выполнена, но не закоммичена: git в его окружении был
недоступен. Восстановлена ревьюером из рабочего каталога.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Работа A11 была выполнена, но осталась незакоммиченной в рабочем каталоге
на машине владельца: в origin ушла пустая ветка. Восстановлена ревьюером
из рабочего дерева и зафиксирована здесь.
Содержание:
- agy_subprocess: зонд обнаружения моделей переведён с разбора ошибки
заведомо неверной модели на штатную команду `agy models`;
- antigravity_adapter: убран выдуманный запасной список gemini-2.5-*,
моделей с такими именами у провайдера не существует;
- codex_oauth: добавлен refresh_codex_token — обновление по refresh_token,
которого не было вовсе;
- quota_collector: source больше не заявляет provider_api там, где ни
одна корзина не измерена; применено к antigravity и opencode-go;
- hermes_plugin: правка обработки роли, поверх сохранённой 2d62d39.
Исправлено при фиксации: тест test_opencode_shows_published_limits
закреплял прежнюю семантику source и падал. Приведён к честной:
source описывает происхождение чисел, а не факт ответа провайдера;
информация об ответе сохраняется в unavailable_reason.
Проверено исполнением: `agy models` сейчас нестабилен и висит даже при
прямом вызове (rc=124 по таймауту 100 с) — зонд честно возвращает пусто
и сохраняет кэш, а не выдумывает список.
Тесты: 303 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверено исполнением на живой конфигурации владельца.
Принято:
- миграция конфигурации работает: 16 -> 22 профиля, claude и grok
получили по 3 слота, find_free_slot возвращает существующие профили по
всем пяти провайдерам. Это снимает корень жалобы «при подключении
грока ошибка»;
- резервная копия router_profiles.yaml.bak_<ts> создаётся, десять
профилей antigravity не изменены ни в одном поле, комментарии не
потеряны;
- квоты grok и opencode-go честно отдают None с причиной вместо
правдоподобных чисел;
- флаг /repair и /reinstall задействован (строка 979), предупреждение
CS0219 при сборке исчезло;
- граница зоны Codex не нарушена, правка плагина 2d62d39 сохранена.
Исправлено при слиянии:
1. Служба обнаружения моделей была недостижима. A9 создал
model_discovery_service.py, интерфейс импортирует model_discovery.
Импорт обёрнут в except ImportError, поэтому расхождение не давало
ошибки — выбор моделей просто оставался пустым навсегда. Добавлена
согласованная точка входа model_discovery.py.
2. Служба отдаёт discovered_at, каталог искал fetched_at/updated_at.
Каталог научен понимать discovered_at.
3. tests/test_ui_routing_graph.py закреплял выдуманный список моделей
("grok-3"). A9 верно убрал литералы, и тест начал падать. Тест
приведён к честному поведению: до обнаружения профиль остаётся без
моделей. Файл в зоне Codex, которому A9 запрещено было её трогать.
Тесты: 307 passed, 2 skipped, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codex остановился на исчерпании лимитов. Влито то, что сделано:
- выбор модели: ui/model_catalog.py с честным пустым состоянием;
- карточка кликабельна целиком (cursor=hand2 + Button-1), «три точки»
перестали быть единственным входом;
- окно настроек роли: _open_agent_settings_modal;
- правая панель «Статус в реальном времени» убрана, центр расширен;
- жёсткие срезы providers[:3] и agents[:5] в диаграмме устранены;
- причины у части Н/Д через unavailable_reason.
Не сделано и уходит в задание: компактные карточки аккаунтов
(accounts_view.py не тронут) и разбор дублирующих разделов
«Провайдеры»/«Квоты» (quotas_view.py не тронут).
Проверено: слияние без конфликтов, правка плагина 2d62d39 сохранена,
290 passed, ruff чисто. Падает только известный нестабильный Tk-тест,
воспроизводится на чистом main.
Каталог моделей сейчас честно пуст: он импортирует
router/model_discovery.py, которого в репозитории нет.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Hub подключён к Hermes как middleware llm_execution и срабатывает на
каждом обращении к модели. Но Hermes роль не передаёт: в kwargs есть
model, provider, session_id, task_id — role нет. resolve_role поэтому
сваливается в роль по умолчанию, и КАЖДЫЙ вызов Hermes маршрутизируется
как orchestrator.
Цепочка orchestrator у владельца исчерпана целиком:
ag-orch-fallback skipped_unhealthy
codex-orch 429 «account is not active, check billing»
opengo-3 No API key found
ag-w1/ag-w3 agy authentication failed or timed out
Роутер возвращал «⚠️ Hermes Router Failover Exhausted» как ответ
ассистента, и Hermes показывал это вместо ответа модели, хотя его
собственный провайдер работал. Это и есть «основной оркестратор не
выбрался» из отчёта владельца.
Теперь при router_error вызов уходит дальше по цепочке (next_call),
а отказ пишется в журнал уровнем warning с полным следом. Плагин обязан
быть незаметным при отказе: он может улучшить маршрутизацию, но не имеет
права сделать Hermes хуже, чем без него.
Проверено исполнением: Hermes получает ответ провайдера, а не текст
ошибки. Тесты: 287 passed (падает только известный нестабильный Tk-тест,
воспроизводится на чистом main).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Сводит две работы: A8 (Antigravity — запуск, развёртывание, самопроверка)
и codex/usability-fixes (B6 граф маршрутизации + B7 дефекты живого прогона,
плюс живой сбор квот).
Проверено исполнением на реальных аккаунтах владельца: квоты Antigravity
теперь приходят от провайдера (source=provider_api) по всем шести
авторизованным профилям с разными числами — ag-w2 показывает 37.4%
остатка недельного пула Claude/GPT. OpenCode Go честно отдаёт None
с причиной.
Разрешение конфликтов:
1. do_test_profile — оба агента чинили P0-3 по-разному. Сохранены обе
правки: проверка просроченной авторизации (A8) поверх локальной
проверки runtime без вызова модели (Codex).
2. _finish в мастере — взята содержательная версия Codex (проверка слота,
создание определения профиля, внесение в маршрутизацию, сброс
cooldown), но её хвост обёрнут так, чтобы сбой в журналировании или
on_complete не оставлял окно открытым. Регрессия 7090c8a закрыта
тестом и продолжает проходить.
Исправлено при слиянии: A8 проверял status.get("expired"), тогда как ключ
называется is_expired. Проверка была мертва изначально — её прикрывал
контроль в адаптере, и это вскрылось только когда Codex убрал вызов
адаптера из «Теста»: протухший аккаунт получал зелёную галочку.
Тесты: 288 passed, 2 skipped, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
_finish первой же строкой вызывал EventLogService.log_event — метода с
таким именем у сервиса нет, есть log(category, message, details, level).
AttributeError уходил в обработчик Tk, а под pythonw консоли нет, поэтому
для владельца кнопка просто не работала: окно оставалось открытым,
on_complete не вызывался, аккаунт не попадал в маршрутизацию.
Вызов приведён к настоящему API. Журналирование и обратный вызов
обёрнуты так, чтобы сбой в них не запирал пользователя в мастере, —
закрытие окна не должно зависеть от побочных действий.
Воспроизведено и проверено исполнением: до правки winfo_exists=1 и
on_complete не вызван, после — окно уничтожено, результат передан.
Тесты: 252 passed в venv Hermes. Единственный сбой
(test_oauth_lifecycle::test_f_copy_before_open_browser, TclError) и
FAILED релизного гейта воспроизводятся на чистом main и к этой правке
отношения не имеют.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>