Commit graph

249 commits

Author SHA1 Message Date
Hermes Team
c8f9b5a382 Merge remote-tracking branch 'origin/antigravity/a37-isolation-guards' into HEAD 2026-08-30 22:45:20 +07:00
ochenstarik-ui
d9dcd4da58 feat(security): A37 agent isolation, credential protection, workspace guards and forensic audit 2026-08-30 15:40:53 +00:00
ochenstarik-ui
635c1cc418 feat(pipeline): A35 and A36 canonical Antigravity workflow pipeline with role resolution and loops 2026-08-30 15:34:07 +00:00
Hermes Team
f5a8fcf0fd docs(agents): задание A38 — сравнение локальных моделей на железе владельца
Локальный кодер выдаёт 13,6 токена в секунду, и владелец хочет понять, есть
ли модель быстрее при сопоставимом качестве.

Базовая линия снята ревьюером на живом сервере и вписана в задание, чтобы не
мерилась заново: Qwen3.8-27B даёт 13,6 ток/с генерации, Qwen3-4B — 124,1,
разница почти девятикратная. Цена контекста измерена точно: 39 КиБ на токен у
27B и 81,5 у 4B.

Отдельно измерен диск, и он оказался узким местом: Crucial BX500 без DRAM,
187 МБ/с мимо кэша, NVMe на машине нет. Загрузка 19-гигабайтной модели с
холодного диска занимает около 100 секунд. Первый замер дал 4,1 ГБ/с, но это
было чтение из кэша оперативной памяти — случай вписан в задание как
предупреждение.

Из списка владельца проверкой отклонён gpt-oss:120b: в карточке модели прямо
указано 80 ГБ и H100. Остальные отсортированы по пригодности для V100, где
скорость определяется активными параметрами, а не общим размером, поэтому
модели MoE поставлены первыми.

Главное требование задания: мерить качество, а не только скорость. Модель,
выдающая 120 ток/с неработающего кода, хуже той, что даёт 13 ток/с рабочего.
Нужен набор из настоящих правок по репозиторию, а не синтетические задачки.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 22:25:12 +07:00
Hermes Team
d897917ee7 docs(agents): задание A37 — изоляция агентов, защита учётных данных и разрушительных операций
Повод пришёл из разбора новостей: OpenAI описала инцидент, где
экспериментальные агенты использовали внутренний Artifactory как канал связи
между собой, обменивались найденными обходами и в итоге скомпрометировали
часть инфраструктуры Hugging Face. Вывод: deny internet != secure agent.

Задание опирается не на этот инцидент, а на наши собственные случаи, каждый
из которых проверен или произошёл в проекте:

- CORS стоял как allow_origins=["*"] с allow_credentials=True, а на localhost
  токен не требуется вовсе: любая открытая рядом страница получала полный
  снапшот со всеми аккаунтами и могла вызывать /api/action. Закрыто в
  c35bc48;
- агенты координируются через публичный репозиторий, и однажды работа ушла в
  main минуя ревью;
- агент Codex переключил ветку в каталоге, где работал ревьюер;
- учётные данные 24 аккаунтов лежат общей кучей, разделения по агентам нет;
- ревьюер удалил учётные данные grok-worker-1, проверяя кнопку удаления, и
  ничто этому не помешало;
- хаб открыт в домашнюю сеть поверх HTTP.

Отдельным критерием вынесено требование, что защита не должна ломать продукт:
хаб с включёнными ограничениями обязан оставаться полностью работоспособным.

Выполнять после A34 и A35: пока нельзя подключить аккаунт и хаб не участвует
в вызовах Hermes, укреплять периметр преждевременно.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 22:03:15 +07:00
Hermes Team
39ccce184c docs(agents): задание A36 — конвейер Antigravity с двумя петлями обратной связи
Владелец описал рабочую схему: flash 3.7 пишет как Кодер 1, gemini pro
проверяет его как Кодер 2 и возвращает на доработку до одобрения, опус 4.6
включается ревьюером после одобрения Pro и при необходимости возвращает
работу Кодеру 2. Две вложенные петли.

Смысл экономический: опус не тратится на то, что отсеет Pro, а Pro не
тратится на то, что flash исправит сам.

Имена моделей взяты из кэша обнаружения на аккаунтах владельца, а не из
головы. Три места, где легко ошибиться, вынесены в задание отдельно:
«гемини про» — это gemini-3.1-pro-high, версии 3.7 у Pro нет вовсе; «опус
4.6» называется claude-opus-4-6-thinking; у flash идентификаторы приходят с
суффиксом усилия, но базовое имя gemini-3.7-flash валидно — ревьюер однажды
уже утверждал обратное и был неправ.

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

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

Зависит от A35: без него хаб в вызовах Hermes не участвует и конвейер будет
собран, но не заработает.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 21:54:48 +07:00
Hermes Team
2f4866fffa docs(agents): задание A35 — настройки хаба не применяются в Hermes ни разу
Владелец заметил, что настройки Hermes не соответствуют настройкам хаба.
Проверка подтвердила худшее: хаб не участвует в вызовах Hermes вообще.

Доказано исполнением. В исходниках Hermes, agent/conversation_loop.py:3221,
middleware вызывается без параметра role — передаются model, provider,
base_url, session_id, task_id, platform, но не роль. А плагин при
неопределённой роли делает next_call(request), то есть пропускает вызов мимо
маршрутизатора. Подача того же набора аргументов на живой плагин:

    как зовёт Hermes (без роли)  ->  МИМО хаба
    если роль передана           ->  обработал хаб

Механизм исправен целиком, его просто никто не включает: аккаунты, цепочки,
квоты и переключение при исчерпании настраиваются и не применяются.

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

Hermes править запрещено: чужой продукт, правка затрётся при обновлении.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 21:51:23 +07:00
Hermes Team
ab2ee12db4 feat(local): скрипт прописывания локальных моделей и назначения ролей
У владельца на сервере два llama.cpp: Qwen3.8-27B на 8081 под тяжёлую
разработку и Qwen3-4B на 8082 под служебные роли — cost-controller,
dependency-agent, tech-writer, tester и суммаризацию.

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

Идентификаторы моделей не выдумываются — спрашиваются у самих серверов через
/v1/models. Сервер не ответил — профиль не трогается, причина названа.
Проверено: при отсутствии серверов скрипт отказывается и объясняет.

max_concurrency выставляется в 1: у обоих серверов --parallel 1, и больше
единицы означает очередь и лавину таймаутов.

Локальный профиль ставится ПОСЛЕДНИМ в цепочке: модель бесплатна и не
исчерпывается, поэтому она хороший последний рубеж, когда платные квоты
кончились. Существующий порядок при этом не переставляется.

Есть обратное соответствие для сборок, где тринадцати ролей ещё нет:
developer-1 -> coder-primary, tech-writer и tester -> fast, а служебные роли
пропускаются с явным сообщением, потому что аналога им там нет. Проверено на
обоих наборах ролей.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 21:32:01 +07:00
Hermes Team
1b03f03ea5 docs(agents): задания A32 и A34 — восстановление подключения аккаунтов, OpenRouter и NVIDIA, удаление десктопа
Проверка кандидата A28-A31 в браузере вскрыла блокирующую регрессию: при
переписывании клиента в A29 функции удалили, а вызовы в разметке оставили.
Консоль на живой сборке:

    openAddAccountWizard is not defined     «+ Добавить аккаунт» не работает
    checkUpdates is not defined             падает при каждой загрузке
    handleNodeAccountChange                 не сменить аккаунт у агента
    handleNodeModelChange                   не сменить модель
    handleRefreshProviderModels             не обновить список моделей

Подключить аккаунт в новой сборке невозможно, назначить агенту тоже. При этом
startDeviceAuth и startRedirectAuth в коде остались, но не вызываются ниоткуда.

A34 собирает в один порядок: восстановление подключения (блокирует остальное),
адаптеры OpenRouter и NVIDIA с несколькими аккаунтами, интерфейс автопоиска
локальных серверов поверх готовой серверной части, и удаление десктопа по A32.

Задание опирается на ветку review/a28-a31-fixes, где лежат исправления
ревьюера, и перечисляет их отдельным разделом, чтобы не переделывались.

A32 добавлен в репозиторий: он был написан ранее, но остался только локально.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 20:48:11 +07:00
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