Commit graph

290 commits

Author SHA1 Message Date
Hermes Team
529192baec fix(roles): 19 агентов вместо 13, шесть пар неотличимы по названию; поиск локальных серверов
Проверка кандидата 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>
2026-08-30 20:46:25 +07:00
Hermes Team
fb5df0efde Merge remote-tracking branch 'origin/main' into HEAD 2026-08-30 20:22:07 +07:00
Hermes Team
7e83c38340 perf(antigravity): из десяти аккаунтов одновременно работал один
В 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>
2026-08-30 20:04:52 +07:00
ochenstarik-ui
93d8f7a1e5 fix(test): isolate agy lookup in hermetic credential test
Clean Windows runners have no agy binary. Point AGY_EXE_PATH at a
dummy file so the test still checks subprocess env sanitization.
2026-08-27 12:08:14 +00:00
ochenstarik-ui
b4ae08ef53 fix(ci): stop hanging hermetic tests and unblock clean/headless runners
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.
2026-08-27 12:03:37 +00:00
Hermes Team
59d57a41ef feat(a31): preflight dependency agent, workflow run state recovery, local concurrency and context guard, PII email masking, and cost-controller token honesty 2026-08-26 09:27:36 +07:00
Hermes Team
8b8aebf928 feat(integration): consolidate A28, A29 and A30 on top of origin/main with green release gate 2026-08-26 09:10:10 +07:00
Hermes Team
620c862912 merge: A29 — design system, themes, routing drag-and-drop 2026-08-26 08:53:17 +07:00
Hermes Team
0bd45a1c39 merge: A28 — subagents and role registry 2026-08-26 08:52:53 +07:00
Hermes Team
f5d4826e7d merge: A30 — workflow canvas, agent files, live workspace 2026-08-26 08:52:38 +07:00
Hermes Team
32bf2c9ac6 test(web): align overview parity with workflow canvas 2026-08-26 08:40:20 +07:00
Hermes Team
83233c891e feat(web): add workflow canvas and live agent workspace 2026-08-26 08:40:20 +07:00
Hermes Team
c35bc4868d fix(security): любой сайт во вкладке рядом мог управлять хабом
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>
2026-08-25 19:57:05 +07:00
Hermes Team
a8c37ca6e3 A29: Design System and Routing UI Drag-and-Drop 2026-08-25 19:53:24 +07:00
Hermes Team
b149a6ab73 Finish A28: Subagents and role registry implementation 2026-08-25 19:38:03 +07:00
Hermes Team
d6ec34d482 docs(agents): задание A31 — проверка готовности, состояние прогона, батчинг, персональные данные
Владелец передал набор описаний субагентов. Ревьюер сверил каждое с кодом:
большая часть уже реализована в Hermes Hub и сильнее шаблонов. Model Router —
это сам Hub; Retry & Fallback — router_engine с цепочками и порогами квот из
A26; Coordinator в части конфликтов доступа — LeaseManager с max_concurrency,
настраиваемым на профиль. Всё это внесено в задание таблицей «не
реализовывать заново», чтобы к вопросу не возвращались.

В работу вошло только отсутствующее:

- роль «Проверяющий готовность»: проект терял раунды на коде 12 установщика,
  неустановленных fastapi/uvicorn, обязательном --effort и отсутствующем
  ag_slot_oauth.py — всё это выяснялось посреди прогона;
- состояние прогона для workflow из A30, как механика, а не роль;
- ограничение одновременных вызовов и контекст для локальных моделей: на
  сервере владельца два llama.cpp с --parallel 1 и почти исчерпанной памятью;
- маскирование почт: sanitize_snapshot вычищает секреты, но слово email в
  server.py не встречается ни разу, а хаб теперь открыт в домашнюю сеть
  поверх HTTP.

Отдельно зафиксировано для роли контроля затрат: поля токенов в телеметрии
есть, но провайдеры их не отдают, поэтому расход можно только оценивать — и
оценку нельзя выдавать за измерение.

Выполнять после A28, A29 и A30.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 19:26:12 +07:00
Hermes Team
1a21c8b1a8 docs(agents): задания A28, A29, A30 — новый фронтенд и субагенты
Владелец передал готовый дизайн: брендбук с тремя темами, макеты главного
экрана, маршрутизации и остальных разделов, логотипы. Плюс список из
двенадцати субагентов с расписанными обязанностями.

Объём не помещается в одно задание, поэтому разделено на три с явными
границами по файлам:

  A28  реестр ролей и двенадцать субагентов          Antigravity, основа
  A29  дизайн-система и экран «Маршрутизация»        Antigravity
  A30  главный экран: граф workflow, LIVE, файлы     Codex

Каждое опирается на состояние, снятое исполнением, чтобы агенты не
переписывали работающее и не выясняли заново:

- новая роль уже добавляется через конфигурацию и доходит до снапшота, но
  подпись падает в сырой идентификатор, потому что таблицы имён зашиты в
  четырёх местах;
- workflow, связей между агентами и файлов агентов в проекте нет вовсе —
  это разработка с нуля, а не доработка;
- источники данных для дашборда существуют и настоящие, параллельный
  заводить не нужно;
- веб-слой без сборки, и это обосновано в контракте.

В каждом задании отдельно оговорено, что демонстрационные значения с
макетов (12 задач, 3.42 с, 94.2%, account-01) — иллюстрация и в код попасть
не должны. Поверхность для выдуманных данных здесь самая большая за проект.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 19:05:56 +07:00
Hermes Team
d5429da63d merge: A27 — обновление из самой программы
Проверено ревьюером исполнением:

  настоящий запрос к релизам      коммит a1e1db7 прочитан из релиза
  установлен тот же коммит        «Установлена последняя сборка»
  короткий SHA против полного     сравнение по префиксу верное
  сеть недоступна                 отказ, а не «обновлений нет»
  предел частоты GitHub API       отказ с внятной причиной
  в интерфейсе                    отметка в шапке, сборка, время проверки
  отказ в интерфейсе              «Ошибка проверки», не «Актуально»

Три дефекта найдены и исправлены ревьюером, подробности в c98806b: функция
была нерабочей в вебе целиком (ответ без данных), контрольная сумма
пропускалась молча при недоступном checksums.txt, перезапуск обещался, но не
выполнялся ни на одной платформе.

Правка .gitignore (artifacts/) законна и безобидна.

450 passed, 2 skipped; ruff чисто.
2026-08-25 18:32:07 +07:00
Hermes Team
c98806b08f fix(updater): проверка обновлений не доходила до интерфейса; непроверенный файл запускался
Проверка 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>
2026-08-25 18:31:52 +07:00
Hermes Team
d7ad3e3165 feat(updater): in-app updates based on main repo release commits (A27 Pass 1) 2026-08-25 17:54:51 +07:00
Hermes Team
7b7b527103 docs(agents): задание A27 — обновление из самой программы
Владелец обновляется вручную на трёх машинах. Механизм обновления в проекте
есть, но мёртв в каждом звене, и это проверено: веб-клиент check_updates не
вызывает вовсе (0 вхождений); DEFAULT_UPDATE_URL смотрит на заброшенный
второй репозиторий, чей манифест застыл на 0.1.1 от 21 августа; версия
захардкожена в 0.1.1 и подниматься не должна, поэтому сравнение по semver
никогда не скажет «есть обновление».

Рабочая часть — apply_update_sync с резервной копией, py_compile-проверкой и
откатом — сохраняется, переписывать её не нужно.

В задании принято решение, которое агентам не следует угадывать: признак
новизны — коммит, а не версия, источник — релизы основного репозитория, куда
поставка идёт на самом деле.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 17:39:26 +07:00
Hermes Team
a1e1db75bf merge: A26 — аккаунты без слотов, распределение по агентам и пороги квот
Проверено ревьюером исполнением, а не по отчёту:

  пятый аккаунт провайдера        codex-4, codex-5, codex-6 создаются сами
  «Авто», один провайдер          ag-w1 встал во все шесть ролей
  «Авто», два провайдера          порядок следует config/router_profiles.example.yaml
  порог, квота неизвестна         состояние не меняется — выдумки нет
  порог, квота 50% при пороге 10  healthy
  порог, квота 4%, режим switch   quota-exhausted
  восстановление до 80%           healthy сам, без вмешательства

Последнее закрывает урок A23: у AUTH_REQUIRED не было выхода и шесть
профилей выпали навсегда; здесь выход есть и проверен.

Найден и исправлен ревьюером один дефект: признак «подключён» пропускал
холодный резерв и пустые слоты, из-за чего страница аккаунтов не пустела, а
«Обзор» предлагал назначать роли на слоты без учётных данных. Подробности в
4c2594b.

Побочная правка tests/test_deployment_doctor.py законна: старый тест требовал
find_free_slot() is None при заполненных слотах — ровно то поведение, которое
A26 отменяет.

442 passed, 2 skipped; ruff чисто.
2026-08-25 17:17:48 +07:00
Hermes Team
4c2594bcf6 fix(web): «только подключённые» пропускало холодный резерв и пустые слоты
Проверка 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>
2026-08-25 17:17:29 +07:00
Hermes Team
b354ac9e6d feat(router): accounts without slots, overview role assignment and quota thresholds (A26) 2026-08-25 16:36:59 +07:00
Hermes Team
8cb57dd6a1 docs(agents): задание A26 — аккаунты без слотов, распределение и пороги квот
Владелец сформулировал, ради чего задумывался хаб: вручную распределять
аккаунты по агентам и следить за квотами, подставляя другой аккаунт там, где
лимит подходит к концу. Модель слотов этому мешает — она навязывает
внутреннее устройство конфигурации и уже привела к тому, что подключённый
аккаунт не появился в маршрутизации.

Задание опирается на три факта, снятых исполнением, чтобы агенты не
переписывали работающее: один профиль уже может обслуживать все шесть ролей;
произвольный идентификатор профиля регистрируется, то есть потолок в три
аккаунта Codex держит только зашитый список в auto_assigner; порогов квот в
коде нет вовсе.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 14:40:37 +07:00
Hermes Team
d4b99a49ef fix(web): мастер не говорил, участвует ли слот в маршрутизации
Владелец подключил аккаунт, увидел его в «Аккаунтах» — и не нашёл ни в
«Обзоре», ни в «Маршрутизации».

Поведение верное: эти два экрана показывают цепочки ролей, а в умолчаниях в
цепочках участвуют только 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>
2026-08-25 14:31:21 +07:00
Hermes Team
681c899cfd fix(web): подключённый аккаунт не появлялся в списке
Владелец подключил первый аккаунт 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>
2026-08-25 11:16:27 +07:00
Hermes Team
0800ca94e0 fix(web): кнопка копирования молча не работала по сети, а возврат вёл в тупик
Владелец открыл хаб по сети и не смог войти в 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>
2026-08-25 11:06:08 +07:00
Hermes Team
de7c7132ae fix(web): при 401 интерфейс молчал вместо того, чтобы спросить токен
Владелец открыл хаб по сети и получил пустую панель с повторяющимся тостом
«Ошибка сервера: 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>
2026-08-25 10:55:41 +07:00
Hermes Team
e90f6331a2 fix(launcher): сообщение при запуске не совпадало с настройками; смена токена
Владелец привязал хаб на сервере к сети, а лаунчер всё равно напечатал
«Headless Mode», показал http://127.0.0.1:5800 и посоветовал пробросить порт
через ssh -L. Текст был зашит и настроек не читал: инструкция вела поднимать
туннель к серверу, который уже был виден напрямую. Инструкция, не
совпадающая с реальностью, хуже отсутствующей — тот же класс дефекта, что и
снятая заглушка про «вход через веб невозможен».

Теперь сообщение читает web_api_host из hub_settings.json: при 127.0.0.1
показывает проброс порта и подсказывает про enable_lan_access.py, при
сетевой привязке — реальный адрес из hostname -I и напоминание про токен.

Добавлен --rotate: смена токена понадобилась немедленно, потому что
выданный токен был вставлен в переписку и скомпрометирован. Без флага
поведение прежнее — повторный запуск токен не трогает.

Проверено: без --rotate токен сохраняется, с --rotate меняется; bash -n на
лаунчере проходит; переводы строк LF.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 10:47:29 +07:00
Hermes Team
2ca42a39c4 fix(installer): установщик Linux собирался с CRLF и падал на первой строке
У владельца на сервере:

    install-linux.sh: line 7: $'\r': command not found
    line 31: syntax error near unexpected token `elif'

Причину я назвал верно в 1d5e44a, но закрыл не там. .gitattributes нормализует
переводы строк при записи в git; объекты действительно чистые — проверено
git cat-file. Но сборка идёт на Windows, где рабочая копия хранится с CRLF, а
build_installer_linux.sh копирует именно из РАБОЧЕЙ КОПИИ. К .gitattributes
это отношения не имеет, поэтому сборка 44808bd уехала с \r.

Диагностику дополнительно запутал git show: он применяет преобразование
переводов строк при выводе, из-за чего индекс выглядел испорченным, хотя не
был.

Исправлено там, где надёжно: сборщик нормализует переводы строк в поставке
перед упаковкой. Теперь неважно, как настроен checkout на машине сборки.
Рабочая копия shell-скриптов тоже приведена к LF.

Проверено на распакованной поставке: ни одного .sh или .py с CR (кроме
самого сборщика, который на целевой машине не исполняется), пролог без CR,
bash -n на install-linux.sh и hermes-hub-web.sh проходит.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 10:37:05 +07:00
Hermes Team
44808bdc4e feat(web): скрипт открытия хаба в домашнюю сеть, с подтверждением цели
Владелец хочет попадать в хаб на сервере по сети, а не через SSH-туннель.
Для этого нужны привязка к 0.0.0.0 и токен: без токена при небlocalhost
привязке сервер отказывается стартовать.

Скрипт ДОПОЛНЯЕТ hub_settings.json, а не переписывает: рядом лежат тема,
интервал обновления квот и параметры маршрутизации. Запись атомарная,
повторный запуск сохраняет прежний токен.

Изменение требует явного --yes, а каталог печатается всегда. Причина не
теоретическая: при отладке скрипт молча взял HERMES_HOME из окружения и
записал настройки не в тестовую копию, а в живой хаб рабочей машины,
привязав его к сети. Откачено, наружу ничего не вышло — адрес читается при
старте, процесс не перезапускался, netstat подтвердил только 127.0.0.1.
Предпросмотр по умолчанию закрывает этот класс ошибки.

Попутно install-linux.sh разворачивает scripts/: они входили в поставку, но
на установленной машине не оказывались, поэтому вспомогательных инструментов
там просто не было.

Проверено на копии настроек: предпросмотр не меняет файл, применение
сохраняет прежние ключи, повторный запуск не меняет токен, посторонний
каталог не затрагивается.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 10:27:16 +07:00
Hermes Team
11aa914fbb fix(security): токен сравнивался обычным !=, вопреки контракту
Раздел 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>
2026-08-25 10:21:40 +07:00
Hermes Team
e8aa60b982 fix(web): при недоступном сервере показывался чужой пример как живые данные
Если /api/snapshot не отвечал, клиент молча загружал snapshot.example.json —
63 профиля с почтами user@example.test — и ставил индикатор источника в
зелёное через setSourceIndicator(true, ...). На экране появлялась полная
здоровая панель, не имеющая отношения к этому серверу.

Дефект становится опасным ровно в том сценарии, ради которого делался вход с
другой машины: при работе через SSH-туннель достаточно промахнуться портом
или потерять туннель, чтобы принять пример за собственные данные и,
например, решать по нему, у какого аккаунта кончилась квота.

Осознанная работа с фикстурой сохранена: ?fixture=1 и открытие страницы
файлом. Подстановка вместо ответа сервера убрана — отсутствие данных теперь
читается как отсутствие данных.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 10:14:04 +07:00
Hermes Team
3287565773 feat(web): вход Antigravity и Claude из браузера на любой машине
Мастер подключения писал, что на сервере без экрана вход «через веб-интерфейс
невозможен», и отправлял в консоль по 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>
2026-08-25 09:52:53 +07:00
Hermes Team
1d5e44ab3c fix(git): shell-скрипты уезжали в индекс с CRLF и ломались на Linux
core.autocrlf=true и отсутствие .gitattributes приводили к тому, что
install-linux.sh хранился с CRLF. На Linux это даёт "$'\r': command not
found", а самораспаковывающийся установщик, собранный после checkout на
Windows, ломался бы целиком: пролог обрезает себя по строке-маркеру, а
маркер с \r не совпадает.

Добавлен .gitattributes: *.sh/*.py/*.yaml только LF, *.ps1/*.bat только
CRLF, *.exe/*.ico/*.png помечены binary, чтобы нормализация их не трогала.
Индекс перенормализован.

Проверено: в индексе все четыре shell-скрипта с LF, заголовок MZ у
launcher/*.exe цел, пересобранный dist/hermes-hub-setup.sh не содержит CR
ни в прологе, ни в распакованном install-linux.sh.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 09:12:01 +07:00
Hermes Team
290e23070e feat(installer): самодостаточный установщик для Linux, без копирования репозитория
install-linux.sh читал исходники из $REPO_ROOT, поэтому на каждую машину
приходилось приносить весь клон git. Добавлен build_installer_linux.sh: он
собирает один файл dist/hermes-hub-setup.sh — пролог на shell, строка-маркер
и tar.gz побайтово следом. Пролог обрезает себя по маркеру, распаковывает
хвост во временный каталог и запускает оттуда install-linux.sh. Состав
поставки тот же, что у виндового payload, плюс installer/; виндовые .exe и
__pycache__ из линуксовой поставки вычищаются.

Убран запасной коммит-литерал 'fb23bff' в манифесте: при сборке без git он
подставлял чужой номер сборки, из-за чего установленная версия выглядела
определённой, не будучи ею. Теперь коммит берётся из BUILD_COMMIT, который
кладёт сборщик, иначе — 'не определён'.

Проверено: распаковка проходит, в поставке лежат свежие server.py и app.js,
секретов, ключей и почт в ней нет.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 09:10:32 +07:00
Hermes Team
158210dc01 fix(web): браузер отдавал старый app.js, интерфейс выглядел не обновлённым
Владелец сообщил, что в маршрутизации не переставляются блоки, нигде нет
выбора аккаунта для роли и на «Обзоре» ничего нельзя изменить. На скриншоте
при этом видна кнопка «Изменить цепочку», которой в коде уже нет: 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>
2026-08-25 00:32:42 +07:00
Hermes Team
c49e6eac31 fix(web): окно аккаунта не обновлялось и застревало на заглушке
Владелец открыл карточку 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>
2026-08-24 20:40:13 +07:00
Hermes Team
0df7dba2ce feat(grok): подписка вместо кредитов — квота и модели заработали
Владелец был прав: «апи не нужен». Подтверждено на его аккаунте 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>
2026-08-24 20:29:08 +07:00
Hermes Team
a40bcac3c3 fix(web): удаление аккаунта не работало из интерфейса
Владелец: «удалить так и не могу ненужный». Backend был починен в
d755a07, но кнопка по-прежнему не работала.

Причина в клиенте: он отправлял только profile_id, без provider. Сервер
не знал, в каком каталоге искать auth.json, строил неверный путь и снова
отвечал «удалять нечего».

Идентификатор профиля однозначен, поэтому провайдер теперь берётся из
конфигурации, когда его не передали. Действие работает независимо от
того, кто его вызвал.

Заодно в клиенте: подтверждение перед необратимым удалением и обновление
экрана после — раньше карточка оставалась в прежнем виде, и было
непонятно, сработало ли.

Проверено через веб-API без параметра provider: до — авторизован,
после — нет.

Тесты: 426 passed, ruff чисто.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 20:19:58 +07:00
Hermes Team
98b6e68617 fix(grok): убрать неверное утверждение про «разные кошельки»
Владелец прислал анонс 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>
2026-08-24 20:06:21 +07:00
Hermes Team
6ebea01386 feat(grok): читать настоящую квоту из биллинга вместо «Н/Д»
Владелец спросил, где кончились лимиты, и показал экран: еженедельный
лимит 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>
2026-08-24 19:56:13 +07:00
Hermes Team
d755a07903 fix(accounts): удаление аккаунта рапортовало успех, ничего не удаляя
Владелец подключил не тот аккаунт 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>
2026-08-24 19:17:51 +07:00
Hermes Team
0be0a58a5d feat(web): авторизация по коду устройства для Grok и Codex прямо из веба
Владелец упёрся в заглушку «подключение через веб-интерфейс пока не
реализовано» и не смог подключить 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>
2026-08-24 15:20:05 +07:00
Hermes Team
50e4de5230 fix(grok): опрос device-flow не различал отказ, просрочку и ожидание
Владелец: «не даёт зайти в грок через аутх». 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>
2026-08-24 15:11:54 +07:00
Hermes Team
ad08c17d83 fix(adapters): обработчик ошибок падал сам, скрывая настоящую причину
Найдено при проверке 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>
2026-08-24 14:54:39 +07:00
Hermes Team
9a2c341f15 Merge A24 (маршрутизация как центр управления) и A25 (локальная модель)
Обе работы приняты, проверено исполнением.

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>
2026-08-24 14:33:16 +07:00
Hermes Team
87d83251bb feat(router): integrate local LLM provider (llama.cpp/vLLM/Ollama) with zero quotas (A25) 2026-08-24 14:00:25 +07:00
Hermes Team
965ef1272c feat(web): routing control center with drag-and-drop, inline models, and 7-view navigation (A24) 2026-08-24 10:53:57 +07:00