Ветка installer/a61-live-verification (agy, коммит b0644a3) частично
пересекалась с HUB-1, частично добавляла то, чего в HUB-1 не было. Пункты
взяты по одному, дубли — нет.
## Взято
1. verify_multi_provider_router.py проверял изоляцию пути на ID "ag-w2" —
на живой машине владельца это существующий подключённый профиль, и
"assert not pdir.exists()" падал не из-за бага, а потому что каталог
реального аккаунта и так был на месте (код возврата 12). ID заменён на
заведомо не боевой "ag-probe-isolation-test".
2. HermesHubSetup.cs: CreateStartMenuShortcut/RemoveStartMenuShortcut не
уважали HERMES_HUB_NO_REGISTRY — переменная гасила запись в реестр (A4),
но ярлык в настоящем меню Пуск изолированные тесты всё равно писали.
Добавлена та же проверка, что уже стоит перед записью в реестр. Заодно
LOCALAPPDATA читается из окружения раньше SpecialFolder — расхождение
найдено живым прогоном.
3. test_installer.py: /silent-тест линковался на venv настоящей машины
junction'ом (Windows) или symlink'ом вместо пустых touch-файлов — раньше
проверка живых Win32-зависимостей ничего по сути не проверяла.
4. update_manager.py: запасной перебор известных имён установщика
(hermes-hub-setup.sh/install-linux.sh/HermesHubSetup.exe) до отката на
.zip — подстраховка на случай расхождения определения платформы.
5. release_gate.py: --assets проверял только присутствие файлов. Добавлена
нижняя граница размера (усечённая сборка, найдено вживую) и сверка
SHA-256 каждого установщика с локальным checksums.txt — до всякой
публикации. Своя реализация (agy: только HermesHubSetup.exe, только
argparse-обвязка, несовместимая с --publication-only из HUB-1), но идея
и обе живые находки — его. Проверено полным циклом: собран настоящий
dist/hermes-hub-setup.sh, посчитаны настоящие контрольные суммы,
--assets прошёл на них 12588887 байт, SHA-256 сошёлся.
## Не взято — уже есть шире в HUB-1
- security_guard.py: точечный "$HOME" в тексте команды вместо конвейера
(диалект по команде, ${HOME}, %USERPROFILE%, fail-closed) — версия HUB-1
шире и уже зелёная на настоящем Windows CI.
- test_a59: заглушка os.getuid без проверки ветки Windows (taskkill/wmic) —
версия HUB-1 параметризована на обе ветки.
- Скомпилированные .exe — не переношу: пересборка на Windows после этого
коммита, здесь compилятора нет.
Тесты: 780 -> 781 passed, 2 skipped, 4 deselected. ruff check . чисто.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Найдено сверх задания и сверх аудита.
Каждый прогон Release Pipeline завершался ошибкой — все пять последних,
включая тег текущего релиза v0.1.3-b1. Причина та же, что у красного CI: шаг
Release Gate падал на test_a37_isolation_guards и test_a41_clean_install. До
публикации не доходил ни один прогон, релизы выкладывались мимо конвейера.
Отсюда ловушка. release.yml собирает hermes-hub-<версия>.zip и
update_manifest.json, а update_manager ищет строго HermesHubSetup.exe или
hermes-hub-setup.sh. Настоящие релизы содержат установщики и checksums.txt,
то есть собраны не этим конвейером. Пока тесты были красными, конвейер падал
и ничего не публиковал; как только они позеленели, случайная защита исчезла:
первый же тег опубликовал бы "latest" без установщиков, и любое обновление
отвечало бы "В релизе не найден подходящий файл обновления для текущей
платформы".
Ловушка закрыта до публикации: release_gate.py --assets dist проверяет, что
собранный набор содержит установщик и checksums.txt, и падает с названной
причиной и подсказкой про installer/build_installer.*. После публикации
добавлен шаг release_gate.py --publication-only — строгий режим, ради
которого ворота и разделялись.
Сборку установщиков в release.yml не переписывал: проверяется только
настоящей публикацией по тегу, это решение владельца. Конвейер по-прежнему не
доходит до публикации, но падает теперь с честной причиной вместо чужой.
Отчёт перенесён в agents/done/ по конвенции репозитория.
Тесты: 777 -> 778 passed, 2 skipped, 4 deselected. ruff check . чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Шаг Release Gate падал UnicodeEncodeError'ом на Windows-раннере: отчёт
печатается по-русски, консоль раннера — cp1252. Тот же класс дефекта, что и
в verification-скрипте, и то же лекарство — force_utf8_output до первого
вывода. Проверено прогоном под PYTHONIOENCODING=cp1252.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
P0 аудита, каждый сначала подтверждён исполнением, а не принят со слов.
1. Release Gate объявлял проверку хеша, которой не было. Печаталась строка
PACKAGE_HASH_VERIFIED=True при том, что hashlib в scripts/release_gate.py
не вызывался ни разу: скачивались байты 0-10 через заголовок Range, и
этого хватало, чтобы счесть хеш проверенным. «Проверенным ассетом» при
этом оказывался первый в списке — checksums.txt, а не пакет.
2. Ворота публикации были fail-open. Измерено в трёх условиях: полный обрыв
сети -> PASS, манифест 404 -> PASS, пакет 404 -> PASS. Ворота пропускали
релиз при любом исходе, включая полное отсутствие релиза.
Разделено на офлайновую часть (проверки 1-7: версии, тесты, updater,
статика, секреты, список разрешённых адресов) и Publication Gate: релиз
есть, ассеты есть, пакет скачан ЦЕЛИКОМ, SHA-256 сошёлся с опубликованным
checksums.txt. Публикационные ворота блокируют в режиме публикации
(--publication или HERMES_RELEASE_PUBLICATION_GATE=1); в обычном прогоне
CI, где релиза для ветки нет и быть не должно, результат сообщается как
есть и не блокирует. Неизмеренное называется причиной, а не выдаётся за
проверенное. Проверено на живом релизе v0.1.3-b1: два пакета скачаны
целиком, хеши сошлись.
3. POST /api/action на loopback принимал межсайтовые запросы. Токен там не
требуется, а действие меняет состояние: удаляет учётные данные, чистит
аккаунты, переключает маршрутизацию, запускает входы OAuth. CORS от этого
не защищает — он мешает прочитать ответ, а не отправить запрос. Измерено
на конфигурации по умолчанию: POST с Content-Type text/plain уходит
кросс-сайтом без предварительного запроса, request.json() разбирает тело
независимо от Content-Type, и запрос с Origin чужого сайта без токена
доходил до исполнителя действий.
Проверяется Sec-Fetch-Site, при его отсутствии — Origin против адреса
запроса. Собственный интерфейс, адресная строка и не-браузерные клиенты
работают как раньше. Защита распространена на все пять небезопасных
методов, не только на /api/action.
4. pricing fallback: safe_load вместо safe_dump. dump сериализовал текст
обратно в строку, проверка isinstance(data, dict) не выполнялась никогда,
таблица цен не загружалась ни разу, а except это глушил.
5. Симуляция Linux в тесте stop_running_hub падала на Windows: os.getuid там
не существует.
Тесты: 756 -> 776 passed, 2 skipped, 4 deselected. ruff check . чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Оба красных Windows-джоба CI падали по причинам, воспроизведённым локально.
1. Инвариант A37 не держался на Windows. "rm -rf $HOME/.hermes" проходил
мимо защиты: переменной HOME в окружении Windows нет, expandvars оставлял
"$HOME" как есть, путь переставал быть абсолютным, склеивался с каталогом
проекта и оказывался "внутри разрешённого корня". Зеркальная дыра на
Linux: "%USERPROFILE%\.hermes" и "C:\Windows" проходили так же.
Разбор пути сведён в один конвейер: классификация диалекта shell по самой
команде (а не по системе-хозяину) -> раскрытие распознанных переменных, с
разрешением HOME/USERPROFILE в домашний каталог даже когда их нет в
окружении -> нормализация разделителей -> канонизация -> сравнение с
защищёнными корнями. Каждый несостоявшийся шаг закрывает проход:
непроверяемый путь не считается разрешённым. Через тот же конвейер
пропущены validate_path, is_forbidden_path и is_inside_allowed_root.
2. UnicodeEncodeError ронял verify_multi_provider_router.py на cp1252-консоли
Windows-раннера — падал вывод, не логика. Общий помощник
console_encoding.force_utf8_output ставит UTF-8 на потоки и оставляет
запасной путь, если перекодировать поток нельзя. Той же реализацией
заменён самодельный блок в cli_commands.
Проверено: скрипт проходит 10/10 под PYTHONIOENCODING=cp1252 и ascii.
Новые тесты воспроизводят окружение обеих систем на любой из них и падают
на прежнем guard ровно на дефекте из CI (6 failed), проходят на новом.
Тесты: 739 -> 755 passed, 2 skipped, 4 deselected. ruff check . чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец спросил, откуда 25 каталогов профилей, когда заводил единицы.
Проверено исполнением: обращение к get_profile_dir создавало каталог. Спросили
четыре пути — появились четыре каталога.
В коде зашит список «стандартных» слотов — ag-orch-primary, ag-orch-fallback,
ag-1..ag-20, ag-w1..ag-w10 — в двух местах: agy_subprocess и
model_discovery_service. Любой обход этого списка материализовал их все. Отсюда
ag-w1..ag-w4, ag-cold-*, ag-spare-* — владелец их не создавал.
Спросить, где профиль жил бы, и завести его — разные действия. Создание теперь
запрашивается явно, create=True, и это делают только те, кто действительно
заводит профиль.
Тесты и сценарий проверки, опиравшиеся на побочное создание, приведены к новому
поведению: они проверяют изоляцию пути, а не то, что каталог возник сам.
669 passed, 2 skipped; ruff чисто; релизный гейт пройден.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1. P0-1: get_default_router_config() возвращает чистую конфигурацию (0 профилей,
13 канонических ролей с пустыми цепочками). Миграция не внедряет фиктивные профили.
2. P0-2: Учетные данные (~/.hermes/*_profiles/, hub_settings.json) изолированы и
никогда не затрагиваются при сбросе или установке.
3. P0-3: Профили создаются динамически при подключении аккаунтов (ag-1, codex-1, etc.).
Пустые цепочки ролей являются нормальным рабочим состоянием.
4. P0-4: Добавлен экшен reset_router_config и кнопка «Начать настройку заново»
в настройках с подтверждением и созданием бэкапа router_profiles.yaml.bak_<timestamp>.
5. P0-5: scripts/verify_multi_provider_router.py адаптирован и проходит 10/10 PASS
как на пустой конфигурации, так и на заполненной.
6. tests/test_a41_clean_install.py: 6 тестов, 490 passed, ruff чисто.
Владелец получил «Ошибка установки (Код: 12)». Код 12 — провал скрипта
scripts/verify_multi_provider_router.py, который виндовый установщик запускает
после развёртывания. На Linux он не запускается, поэтому там всё вставало.
Скрипт пережил три изменения продукта и не был под них обновлён:
1. Требовал роль "orchestrator". A28 переименовал её в "manager", и проверка
падала на первом же шаге. Теперь актуальное имя спрашивается у реестра
ролей, а не помнится в скрипте.
2. Требовал непустую цепочку у КАЖДОЙ роли. A28 добавил роли, объявленные без
реализации — guardian и cost-controller, — у них аккаунтов ещё нет.
Установка падала из-за роли, которой никто не пользуется. Теперь пустая
цепочка допустима и лишь отмечается, а обязательна она только у
оркестрирующей роли: без неё маршрутизация действительно не работает.
3. Зашивал порядок цепочки codex -> antigravity -> opengo-3 и конкретные
идентификаторы профилей. Но порядок — выбор владельца, он меняет его мышью,
и любая перестановка роняла установку. Проверка переписана на механизм:
берётся настоящая цепочка, роняются все профили кроме последнего
достижимого, и проверяется, что маршрутизатор дошёл именно до него.
Учтён предел max_failover_attempts — за него цепочка не проходится.
Карта адаптеров дополнена claude, grok и local: раньше в ней были только три
провайдера, и хвост цепочки из остальных не покрывался.
Проверено на конфигурации владельца: 10/10 CHECKS PASSED, код возврата 0.
486 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
У владельца на сервере два 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>
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.
Владелец привязал хаб на сервере к сети, а лаунчер всё равно напечатал
«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>
Владелец хочет попадать в хаб на сервере по сети, а не через 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>
Владелец получил «Ошибка установки (Код: 12)» на чистой машине.
Причина: scripts/verify_multi_provider_router.py, который установщик
запускает после развёртывания, требовал РОВНО 16 профилей и дословно
заданные цепочки ролей. Миграция из A9 законно доводит конфигурацию до
22 профилей, добавляя claude и grok. Проверено: скрипт падал с
«Expected 16 profiles, got 22», то есть установка обрывалась на любой
машине, где миграция отработала.
Проверки переписаны структурными: есть ли профили у каждого провайдера,
непусты ли цепочки ролей и ссылаются ли они только на существующие
профили. Смысл проверки — работоспособна ли маршрутизация, а не совпадает
ли конфигурация с зафиксированной когда-то. Скрипт проходит 10/10.
Отдельно: код 12 возвращался и при отказе проверки, и из общего catch —
владелец видел число без причины. Непредвиденный сбой отделён в код 15,
обе ветки теперь пишут пояснение в интерфейс установщика.
Тесты: 373 passed, ruff чисто. Установщик пересобран.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Implemented 3-tier release feed status (MANIFEST_LIVE, PACKAGE_LIVE, PACKAGE_HASH_VERIFIED)
- Added reproducible package and checksum builder in scripts/build_dist.py
- Preserved user header comments across YAML saves in router_config.py
- Connected model_timeout_seconds, monitoring_interval_seconds, and auto_monitoring to runtime
- Guarded global gemini:antigravity credential swap with _AGY_INVOCATION_LOCK to eliminate concurrent subprocess race
- Added concurrency regression test in tests/test_antigravity_concurrency.py
- Added interprocess file locking (_FileLock) for router_state.json in health_tracker.py
- Sandboxed APPDATA and USERPROFILE in tests/test_installer.py
- Exported roadmap modules in router/__init__.py
- Verified 151 passing tests (100%) and 7/7 release gate checks
`ruff check .` failed on every push since CI was added (1221 findings), so the
job aborted before pytest and the release gate ever ran. Adds [tool.ruff]
selecting bug-catching rules only (E9, F63, F7, F82, F811); style and
modernization rules stay off until their ~1200 findings are burned down.
That selection immediately surfaced seven live defects:
- hermes_hub_app: HubModal used but never imported, so the "Назначить" role
modal raised NameError — the handler added to close an earlier review
finding could never open.
- hermes_hub_app: three `after(0, lambda: ...(e))` callbacks referencing the
except-bound name, which Python unbinds at block exit, so every UI error
path raised NameError instead of reporting the error.
- auto_assigner: build_team_hierarchy referenced an undefined `is_main` and
crashed on every call.
- ui/assets and live_provision_and_validate: annotations naming Any/Tuple
without importing them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Implemented HubSnapshot and central HubStateStore for normalized in-memory state caching (<0.05ms)
- Refactored AccountsView and RoutingView with reusable AccountCardWidget and RoutingRoleWidget to eliminate widget recreation
- Implemented central HermesRefreshScheduler with 5s tick, concurrency throttling, dedup, and spread initial delays
- Added typed EventBus with thread-safe UI main loop dispatching via root.after
- Implemented dynamic ModelRegistry with capability-based role requirements and multi-dimensional scoring
- Integrated Antigravity separate quota buckets (Claude vs Gemini) and same-account model fallback
- Enhanced SessionAffinityTracker with TTL expiration and LRU capacity bounds
- Eliminated long subprocess holding of _CM_LOCK and ensured Windows credential restoration in finally block
- Added FastAPI REST contracts in gui_server.py as foundation for future Tauri frontend
- Verified 100% pass across all 91 pytest tests and 7/7 release gate criteria