- Реализован запуск agy в терминале с изолированным HOME (x-terminal-emulator, gnome-terminal, konsole, xfce4-terminal, tilix, alacritty, kitty, terminator, urxvt, foot, xterm / Windows wt, cmd)
- Опрос появления antigravity-oauth-token вместо ожидания процесса терминала
- Защита занятого слота от перезаписи без подтверждения
- Честное извлечение email без выдумывания identity и предотвращение дубликатов слотов
- Сохранён браузерный OAuth в качестве запасного пути
- Добавлены 12 тестов в test_a57_agy_native_login.py, 640 тестов проходят
В карточке владельца рядом стояли «Проверен: не работает — Please sign in» и
«Получено 11 моделей · 13:25:53». Источники разные: красная строка берётся из
состояния проверки, список — из кэша каталога, и обновляются они независимо.
Каталог был получен позже отказа, то есть провайдер с тех пор ответил, а
карточка продолжала утверждать обратное.
Теперь, если каталог получен без ошибки и позже неудачной проверки, вердикт
показывается как устаревший с предложением проверить заново. Объявлять аккаунт
рабочим не за что — проверка после этого не выполнялась, и выдумывать её
результат нельзя.
Отдельно установлено: поле commit в /api/health читается из файла на диске при
каждом запросе, поэтому устаревший процесс рапортует свежий коммит. Различать
сборки по нему нельзя.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец видел «сохранение аккаунта и запуск проверки» и ждал. Зависанием это
не было: действие честно дожидалось проверки у провайдера. Для Antigravity она
идёт через CLI и в худшем случае складывается из 90 с на захват замка профиля,
65 на каталог моделей и 90 на пробный вызов — около четырёх минут молчания при
обещанной в интерфейсе «минуте на этап».
Сохранение учётных данных и назначение роли занимают миллисекунды. Теперь
действие возвращается сразу, а опрос провайдера ставится в фон; карточка
обновляется, когда он закончится, — снапшот и так опрашивается по таймеру.
Провайдеры с ключом поведения не меняют: их подключение проверяется
предварительной проверкой до сохранения и возвращается сразу, как требует A54.
Если фоновая служба не работает, проверка по-прежнему выполняется на месте —
иначе результата не будет вовсе.
Надпись в мастере исправлена: обещание «до минуты на этап» не соответствовало
действительности.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
agy 2.0 читает учётные данные не из .gemini/oauth_creds.json — это формат
Gemini CLI. Свой токен он берёт из .gemini/antigravity-cli/antigravity-oauth-token
в виде {"auth_method": ..., "token": {...}}. Хаб этот файл никогда не создавал,
поэтому «Авторизация успешно завершена» соседствовала с ответом
«Please sign in to view available models».
Установлено сравнением рабочего профиля владельца с неработающим: оба имели
oauth_creds.json одинакового размера, различие было только в этом файле.
Значение auth_method («consumer») взято из рабочего профиля, а не выведено
из общих соображений.
Срок годности пишется в обоих видах: expiry_date в миллисекундах для формата
Node и expiry строкой RFC3339 для Go-шного oauth2.Token.
Заодно устранён рост номеров профилей: слот выбирался до входа, когда почта
ещё неизвестна, и повторный вход тем же аккаунтом занимал очередной свободный
слот — один аккаунт владельца расползся на ag-2, ag-3, ag-4. Теперь после
опознания почты учётные данные возвращаются в слот, который этот аккаунт уже
занимает. Функция проверки двойников в проекте была, но её никто не вызывал.
Версия поднята до 0.1.3, чтобы отличать сборки на глаз. Проверка версии в
тестах сверяется с исходником, а не с записанным числом.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Вход. agy читает учётные данные из <каталог профиля>/.gemini/oauth_creds.json.
Хаб этот файл пишет, но при отсутствии токена доступа создавал его пустым, а
сбой записи только заносился в журнал. Владелец видел «подключено», а проверка
отвечала «Please sign in to view available models» — ровно это и случилось с
профилем ag-2 при заново подключённом victor.trushenko@gmail.com.
Теперь вход без токена доступа отвергается с причиной, а несостоявшаяся
запись учётных данных не позволяет считать аккаунт подключённым. Проверено:
данные без токена отклоняются, с токеном проходят.
Версия. Между сборками она не меняется намеренно, а коммит — строка из
шестнадцатеричных цифр, по которой на глаз не отличить старую сборку от
новой. В /api/settings добавлено время установки из манифеста, и оно
показывается рядом с версией: «Hermes Hub Web v0.1.2 · 01.09 04:10».
610 passed, ruff clean, релизный гейт 10/10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Новое сообщение об отказе agy показало причину. У рабочего аккаунта
victor.trushenko@gmail.com каталог получался (11 моделей), а вызов падал:
invalid model selection (--model "gemini-3.7-flash" --effort ""):
gemini-3.7-flash requires --effort (available: low, medium, high)
Каталог agy отдаёт идентификаторы с уровнем усилия внутри имени
(gemini-3.7-flash-high, -medium, -low), а в профиле хранится голое имя.
Вызов уходил с пустым --effort, и agy отказывался работать.
Теперь уровень определяется: если он зашит в имени, отделяется от модели;
если нет — берётся из каталога, предпочтительно medium. Проверено, что
claude-sonnet-4-6 при этом не разбирается ошибочно: -6 уровнем не
является.
Отдельно: восемь профилей Antigravity отвечают «Please sign in to view
available models» при HOME=~/.hermes/agy_profiles/<slot>. Это пустые
заготовки без учётных данных, а не поломка: agy запускается с подменённым
HOME ради изоляции аккаунтов, и в незаполненном каталоге ключей нет.
Подписочные аккаунты Codex и Claude: отказ по API-ключу теперь объясняет,
что такие аккаунты подключаются входом по ссылке, а не ключом. Проверка
обращается к каталогу моделей, где платформенному ключу нужны права
api.model.read, и подписочный токен там получает 401 или 403 при исправном
аккаунте.
610 passed, ruff clean, релизный гейт 10/10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Адрес. Адаптер всегда шёл на 127.0.0.1:11434, получал Connection refused и
помечал аккаунт нерабочим — при том что каталог из девятнадцати облачных
моделей у него получался. У Ollama Cloud локального сервера нет вовсе,
есть только ключ. Теперь при заданном ключе и незаданном адресе адаптер
идёт на ollama.com; явно указанный адрес по-прежнему в приоритете.
Проверено: только ключ даёт https://ollama.com, заданный адрес — его же.
Ключ. Как у NVIDIA и OpenRouter, он читался только из auth_config в
router_profiles.yaml, куда мастер его не кладёт. Добавлено чтение из
хранилища учётных данных.
Лимиты. Провайдер ollama безусловно относился к локальным и получал
подпись «Без ограничений (локальная модель)». Для облачного аккаунта это
неправда: лимиты у него есть. Теперь облачный аккаунт (узнаётся по
наличию ключа) показывает «Н/Д: лимиты не измерены — провайдер не
сообщает их через API».
Отдельно: отказ agy теперь доносит причину. Прежнее «код 1; каталог не
получен» скрывало и текст ошибки, и главное — что agy запускается с HOME,
подменённым на каталог профиля ради изоляции учётных данных. Если вход
делался обычным agy в оболочке, ключи легли в настоящий домашний каталог,
и профиль пуст. Теперь в сообщении и ответ agy, и использованный HOME.
610 passed, ruff clean, релизный гейт 10/10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Каталог NVIDIA публичный: GET /v1/models отдаётся вообще без ключа и
возвращает одни и те же 83 модели всем. Проверено запросом. Полей о
доступности в нём нет — только id, object, created, owned_by. Значит
узнать из каталога, чем может пользоваться аккаунт, невозможно.
Единственный достоверный способ — спросить у самой модели. Недоступная
отвечает «404 Function ... Not found for account <id>» до генерации, то
есть её проверка ничего не стоит; доступная расходует один токен при
max_tokens=1.
Добавлены модуль model_entitlements, действие probe_account_models и
кнопка «Определить доступные модели» в карточке аккаунта для NVIDIA и
OpenRouter. Запуск только по явному нажатию с подтверждением: опрос
тратит вызовы и упирается в ограничения частоты. Результат сохраняется.
Модели раскладываются на три группы, а не на две: доступные, не выданные
аккаунту и НЕ ОПРЕДЕЛЁННЫЕ с причиной. Отвергнутый ключ (401/403),
превышение частоты (429) и сетевой сбой попадают в третью группу —
выдавать их за «недоступно» нельзя. Проверено: без ключа все модели
получают «ключ не задан», с неверным ключом — «ключ отвергнут (HTTP 403)».
610 passed, ruff clean, релизный гейт 10/10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
При проверке подключения ключ читался из window._wiz_token. Переменная
переживает предыдущие попытки подключения и могла оказаться пустой или от
другого провайдера: OpenRouter отвечал «HTTP 401: Missing Authentication
header» при заполненном поле, и причина была не видна.
Прямой вызов validate_connection с ключом даёт «401: User not found», то
есть заголовок формируется верно — дело было в источнике значения.
Теперь ключ и адрес читаются из полей в момент проверки.
609 passed, ruff clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
agy. Поиск проверял только раскладку Windows (%LOCALAPPDATA%/agy/bin) и
PATH. В Linux утилита ставится в ~/.local/bin, а хаб запускается с
урезанным окружением, где этого каталога в PATH нет. Владелец видел «agy
executable not found» при установленной и работающей утилите — which agy
находил её по адресу /home/ochenstarik/.local/bin/agy.
Добавлены стандартные места Linux: ~/.local/bin, /usr/local/bin, /usr/bin,
/snap/bin. Сообщение об отказе перечисляет проверенные пути. Отдельно
различается «нет доступа к каталогу» и «файла нет».
Ollama Cloud. Мастер требовал адрес локального сервера, которого у
облачного аккаунта не существует: там только ключ. Теперь при заданном
ключе недоступность локального адреса не считается отказом — аккаунт
проверяется по каталогу ollama.com и принимается с честной оговоркой,
что локальные модели недоступны. Проверено: 19 моделей каталога.
Без ключа поведение прежнее.
609 passed, ruff clean, релизный гейт 10/10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ключ API. add_account сохраняет его через ProfileAuthManager, а адаптеры
NVIDIA и OpenRouter читали только profile.auth_config из
router_profiles.yaml — туда ключ не попадает. Запрос уходил без заголовка
Authorization, и провайдер отвечал «401: Header of type authorization was
missing», хотя список моделей тем же ключом получался: обнаружение читает
ключ из хранилища, а адаптер читал из конфигурации.
Проверено: auth_config в yaml пуст, адаптер теперь находит ключ в
хранилище учётных данных.
Счётчик аккаунтов. Значок в меню брал readiness.accounts_connected_count
(строго AUTHENTICATED), а карточка на странице считала профили правилом
«не NOT_CONFIGURED». Владелец видел 9 в меню и 3 на странице. Приведено к
одному определению — тому же, что у страницы.
609 passed, ruff clean, релизный гейт 10/10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A48 и A49 добавили workspace.js, workflow.js и workflow.css, но заголовки
Cache-Control им не прописали. Проверено запросом к серверу владельца:
у /, /app.js и /style.css стоит no-cache, must-revalidate, а у
/workspace.js и /workflow.js заголовков кэша нет вовсе.
Из-за этого браузер держал старые файлы после обновления сборки, и
владелец видел прежний интерфейс при новом номере сборки — та самая
поломка, ради которой запрет кэша когда-то вводился для app.js.
Перечисление файлов сделано общим списком, чтобы следующий добавленный
файл не оказался снова без заголовков.
609 passed, ruff clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверялся ровно один путь — $HERMES_HOME/config.yaml. Hermes хранит
конфигурацию по-разному в зависимости от версии и способа установки, и у
владельца на Linux хаб писал «В Hermes: конфигурация не найдена» при
работающем Hermes v0.20.6.
Теперь проверяются семь известных мест, и в сообщении перечисляется, где
именно искали, — владелец видит список и может назвать верный путь.
Отдельно различаются «файла нет» и «нет доступа к каталогу»: закрытый
правами каталог помечается как непроверенный, а не как отсутствующий.
Это тот же класс ошибки, на котором я сам дважды ошибся в диагнозе,
приняв отказ в доступе за отсутствие файла.
599 passed, ruff clean, релизный гейт 10/10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Кэш опознания. _identities и _snapshots в AccountQuotaService живут в
памяти по ключу «провайдер:слот» и при удалении ключа не чистились.
Владелец удалил аккаунт Antigravity, завёл victor.trushenko@gmail.com, а
в списке остался прежний trushenko.semya@gmail.com. Добавлен
forget_profile, вызывается при удалении учётных данных и при перезаписи
слота другим аккаунтом. Проверено: после сброса запись исчезает.
NVIDIA. Проверка подключения получала каталог моделей успешно — то есть
ключ рабочий и аккаунт опознан, — а затем делала пробный запрос к первой
чат-модели каталога. Каталог NVIDIA общий, доступ к конкретной модели
даётся по аккаунту, и ответ «Function ... Not found for account <id>»
объявлялся провалом подключения. Теперь успешный список моделей считается
доказательством работоспособности ключа, а неудачная проба возвращается
примечанием с предложением выбрать доступную модель.
401 и 403 при этом по-прежнему означают отказ: ключ отвергнут. Различие
поймал тест A54 на неверном ключе.
Выход из программы. Неудачная остановка процессов отменяла выход целиком:
владелец нажимал «закрыть» и оставался в работающем приложении. Теперь
показывается предупреждение, а программа закрывается.
599 passed, ruff clean, релизный гейт 10/10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Версия. В index.html номер стоял руками в двух местах, а в app.js был
запасным значением '0.1.1'. Номер сборки приходил из API и обновлялся,
версия — нет: владелец обновился до e6eab12 и увидел v0.1.1. Подъём
версии в пяти местах бэкенда до экрана не доходил вовсе. Теперь версия
берётся только из API; если сервер её не передал, пишется Н/Д с причиной,
а не правдоподобный номер.
Установка. Отказ остановки прежнего хаба прерывал установку целиком, и
владелец получал голый код 15. Теперь неудачная остановка не отменяет
установку: причина показывается, работа продолжается, и если файл
действительно занят, об этом скажет копирование с именем файла.
Копирование файлов получило повтор: процесс мог не успеть отпустить файл
после остановки. Пять попыток с паузой вместо отказа с первой.
599 passed, ruff clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец просил менять версию сборки. Прежнее указание держать 0.1.1
отменено этим.
Поднято во всех пяти местах, где версия объявлена: version.py,
pyproject.toml, HermesHubSetup.cs, install-linux.sh и
config/compatibility.json. Последний ловится тестом на совпадение с
__version__ — без него прогон падал.
599 passed, ruff clean, релизный гейт 10/10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверено ревьюером исполнением:
check_account больше не отказывает из-за незапущенной фоновой службы.
Раньше кнопка «Проверить подключение» перекладывала работу на службу и
возвращала «Фоновая служба проверки не запущена». Теперь выполняется
настоящий запрос, и владелец видит причину провайдера: «Не удалось
подключиться к локальному серверу LLM».
Удаление ключа больше не ждёт полного обхода провайдеров: замерено 0,0 с
против прежних тридцати.
Пустой ключ отклоняется ДО создания слота — профилей-пустышек не остаётся.
Действие clear_accounts: предпросмотр целей, подтверждение, и Antigravity
защищён — проверено, ключ ag-1 после очистки цел, в цели не попадает.
Лаунчер: значок в области уведомлений, при закрытии окна вопрос «Закрыть
Hermes Hub полностью?», выход снимает браузер и останавливает
собственный процесс сервера (StopOwnedRuntime).
Правка ревьюера: поддержка провайдера проверяется ДО требования ключа.
A54 поставил проверку ключа первой, и у неподдерживаемого провайдера
выводилось «не указан API-ключ» вместо «не поддерживается» — ключ там не
поможет, и сообщение уводило не туда. Тест A42 это поймал.
599 passed, ruff clean, релизный гейт 10/10 на обеих конфигурациях.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Хаб — оконное приложение без консоли, поэтому каждый запуск консольного
exe открывал отдельное окно. Пока проверка аккаунтов шла по нажатию, это
было незаметно. A50 сделал проверку автоматической раз в минуту, и окна
agy.exe стали появляться постоянно, мешая работе.
Добавлен hidden_process_kwargs(): CREATE_NO_WINDOW плюс STARTUPINFO с
SW_HIDE, на не-Windows пусто. Применён ко всем ФОНОВЫМ вызовам:
опрос моделей agy, выполнение запроса agy, чтение ключей, codex_oauth,
launcher_bootstrap, git rev-parse и проверка версии в обновлении.
Вызовы, где окно нужно видимым, не тронуты: вход по OAuth сознательно
использует CREATE_NEW_CONSOLE, запуск установщика тоже должен быть виден.
574 passed, ruff clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Сравнение сборок шло только на равенство коммитов: любое расхождение
объявлялось обновлением. Последний релиз на GitHub указывал на 380c218
от 30 августа, у владельца стояла сборка новее — хаб предложил «обновиться»
на старый коммит, владелец нажал и получил откат.
Добавлена проверка по времени: если релиз опубликован раньше, чем
установлена текущая сборка (deployed_at из deployment_manifest.json),
это откат, а не обновление. Такой релиз не предлагается, и владельцу
пишется, что установлена сборка новее опубликованного релиза.
Проверено на фактических данных владельца: релиз 380c218 от 2026-08-30
против установки от 2026-08-31 даёт update_available=False.
Версия остаётся 0.1.1 намеренно, сборки различаются коммитом.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверено ревьюером исполнением: подключение с выбором роли кладёт
аккаунт в её цепочку (developer-1 → ['openrouter-1']). Раньше выбор роли
до цепочки не доходил, и ни один аккаунт владельца ни в одной цепочке не
состоял.
Роль на карточке берётся из живой цепочки: role_assignments.get(pid, [])
вместо подстановки догадки. Таблица DEFAULT_SLOT_ROLES удалена — ссылок
на неё в дереве не осталось.
На маршрутизации показывается «Сейчас ответит: <аккаунт>» либо «никто»
с причиной. В плагине Hermes учитываются вызовы мимо хаба
(record_bypass в четырёх местах, счётчик bypassed_calls_count).
Обратное чтение сделано честно: get_hermes_config_status читает
конфигурацию Hermes ТОЛЬКО на чтение, с отдельным тестом на это.
Разрешение конфликтов с A49 и A50 (ветка A51 отведена от 17b368a):
- action_handler: взят вариант A50 — он надмножество, содержит и
назначение в роль, и защиту слота от чужого провайдера;
- auto_assigner: взят вариант A51 — таблица-догадка удалена;
подпись llama.cpp от A50 сохранена, она вне конфликта;
- settings_service, unified_health, app.js: обе стороны, правки
дополняют друг друга;
- тест свежести памяти: взята строгая проверка, вариант A51 молча
пропускал отсутствие записанного коммита.
Две правки ревьюера по итогам слияния:
- в app.js при сложении потерялась закрывающая скобка блока настроек;
- в add_account сохранение учётных данных вызывалось дважды, и второй
вызов обращался к auth_data, которой у уже авторизованного аккаунта не
существует. Перевод аккаунта в другую роль падал с ошибкой, хотя ключ
вводить не требуется. Дубль убран, оба теста A51 проходят.
554 passed, 1 skipped, ruff clean, релизный гейт 10/10 и на конфигурации
владельца, и на пустой.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверено ревьюером исполнением, все восемь замечаний владельца закрыты:
Чужой слот. Раньше add_account с profile_id=ag-w1 для nvidia возвращал
ok=True и клал аккаунт в слот Antigravity. Теперь отказ с причиной:
«Слот ag-w1 не принадлежит провайдеру nvidia». Свой слот принимается,
идентификатор выдаётся верный.
Группировка по провайдерам восстановлена: скрытие заголовков групп,
добавленное в A48 (display:contents + display:none), убрано.
Автоматическая проверка запускается при старте веб-сервера
(_start_background_refresh), состояние и списки моделей больше не ждут
ручного нажатия.
Облачные модели Ollama: эндпоинт не выдуман — ревьюер проверил запросом,
https://ollama.com/api/tags отвечает 200 и отдаёт 19 моделей
(gpt-oss:20b, kimi-k2.6, glm-5.1 и другие).
Локальный провайдер подписан llama.cpp вместо «Локальный сервер».
Показ хода при долгом опросе честный: «может занять до минуты на этап».
Конфликты с A49 разрешены сложением: правки дополняют друг друга —
поле пути к хранилищу Obsidian и поле периода проверки аккаунтов,
стили вкладки скиллов и стили групп провайдеров. В тесте свежести памяти
взят вариант A50: коммит извлекается из файла, а не зашит.
545 passed, 1 skipped, ruff clean, релизный гейт 10/10 и на конфигурации
владельца, и на пустой.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>