План моста (AI-Memory/00_SYSTEM/AGENTS_BRIDGE_PLAN.md) числит hermes-hub
единственным репозиторием без корневого AGENTS.md. Существующий
agents/AGENTS.md на AI-Memory не ссылается вовсе.
Мост указывает, где лежит память проекта, что прочитать перед работой и
что обновить после, не дублируя уроки и решения.
Отдельно оговорено, что память доступна только на сервере: агент на
другой машине её не видит и обязан назвать это в отчёте.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Экран маршрутизации брал профили из currentSnapshot.profiles, тогда как
/api/snapshot отдаёт dataclasses.asdict(HubSnapshot), где поле называется
all_profiles. Ключа profiles в ответе нет — проверено перечислением полей
датакласса. Это было единственное такое место в файле: остальные девять
обращений уже читали all_profiles.
Следствия, которые чинятся разом:
- правая колонка «доступные аккаунты» была всегда пуста;
- «0 аккаунтов» оставалось литералом из разметки, счётчик не переписывался;
- строки цепочек получали пустой профиль, provider становился 'unknown',
и все аккаунты рисовались одной иконкой-заглушкой.
Для строк цепочек берётся полный список профилей, для колонки «доступные» —
только подключённые (правило A26).
Убрана полоса квоты с зашитым width:80%: она была одинаковой у всех
аккаунтов и ни на чём не основана. Вместо неё индикатор измеренного
состояния; неизвестное состояние остаётся серым, а не выдаёт себя за
здоровое.
Подписи связей на холсте центрируются (text-anchor отсутствовал, поэтому
подпись уходила вправо от середины связи и обрезалась о край холста) и
получают обводку, чтобы читаться поверх линии.
Инспектор агента показывал модели из preferred_models — это настроенный
список предпочтений профиля, а не то, что даёт провайдер; model_states
строится перебором того же preferred_models, поэтому запасная ветка давала
тот же набор. Источником стал discovered_models провайдера. Настроенная у
агента модель теперь всегда присутствует в списке: раньше, если её там не
было, ни один option не получал selected, показывался первый вариант, и
сохранение конфигурации молча подменяло модель агента.
486 passed, ruff clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1. P0-1: get_default_router_config() возвращает чистую конфигурацию (0 профилей,
13 канонических ролей с пустыми цепочками). Миграция не внедряет фиктивные профили.
2. P0-2: Учетные данные (~/.hermes/*_profiles/, hub_settings.json) изолированы и
никогда не затрагиваются при сбросе или установке.
3. P0-3: Профили создаются динамически при подключении аккаунтов (ag-1, codex-1, etc.).
Пустые цепочки ролей являются нормальным рабочим состоянием.
4. P0-4: Добавлен экшен reset_router_config и кнопка «Начать настройку заново»
в настройках с подтверждением и созданием бэкапа router_profiles.yaml.bak_<timestamp>.
5. P0-5: scripts/verify_multi_provider_router.py адаптирован и проходит 10/10 PASS
как на пустой конфигурации, так и на заполненной.
6. tests/test_a41_clean_install.py: 6 тестов, 490 passed, ruff чисто.
Установка на Windows упала с кодом 12, и причина была не в новом коде, а в
накопленном состоянии: конфигурация тащила роль под старым именем, роли без
цепочек и 24 пустых заготовки, переживших несколько переименований. Проверка
споткнулась о наследие.
Владелец сформулировал вывод: при первой установке всё должно начинаться с
нуля — сначала аккаунты, потом распределение по агентам.
Задание разводит два случая: конфигурации нет — первая установка, профилей не
создаётся вовсе; конфигурация есть — обновление, ничего не трогается. Плюс
явная кнопка сброса с подтверждением и резервной копией.
Отдельным пунктом, из-за цены ошибки: сброс касается только маршрутизации.
Каталог agy_profiles не затрагивается ни при каких условиях — потеря учётных
данных означает повторный ручной вход в два десятка аккаунтов, включая
Antigravity со входом по ссылке для каждого профиля.
Требуется также прогнать проверочный скрипт установщика на ПУСТОЙ
конфигурации: он этого случая никогда не видел, у него всегда было 24
профиля, и падение на чистой машине дало бы тот же код 12 новому
пользователю.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец получил «Ошибка установки (Код: 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>
Нормализация в сборщике делалась через sed -i, а на Windows в Git Bash это
ненадёжно. Замена на Python сначала не сработала по неожиданной причине:
python3 здесь — заглушка Microsoft Store, которая печатает "Python" и не
выполняет ничего. Нормализация тихо становилась пустой операцией, а сборщик
при этом рапортовал об успехе.
Теперь интерпретатор выбирается проверкой: python3, python, py — берётся
первый, который действительно исполняет код. Не нашёлся ни один — сборка
прерывается с объяснением, потому что установщик без нормализации ломается
на Linux с "$'\r': command not found".
Проверено побайтово на собранной поставке: в прологе ноль байтов 0d, маркер
на своём месте, среди .sh и .py файлов поставки ни одного с возвратом
каретки.
Отдельно отмечу для истории: тревога о возврате CRLF была поднята по ошибке
измерения — grep -c с шаблоном возврата каретки в этой оболочке давал ложные
срабатывания. Сама поставка была исправна и до правки.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Брендбук требует три темы, причём Medium — отдельная, а не осветлённая Dark:
тёмно-зелёный холст #1A2A1F со светлыми кремовыми карточками #F7F1E3.
В style.css она описана целиком и отрисовывается верно — проверено
подстановкой data-theme вручную. Но в список выбора не попала, а applyTheme
знал только light и dark, поэтому любое другое значение сбрасывало тему в
системную. Выбрать Medium было невозможно.
После правки все три переключаются:
dark фон #101510 карточки #1A2A1F
medium фон #1A2A1F карточки #F7F1E3
light фон #F7F1E3 карточки белые
Цвета совпадают с палитрой брендбука. 486 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
На машине владельца задана ANTHROPIC_BASE_URL=https://api.anthropic.com, и
из-за неё test_claude_health_check_real_probe падал: адаптер строил
".../models" вместо ".../v1/models". Код при этом верен — подставлялось
значение из окружения вместо умолчания.
Проверено: со снятой переменной 486 passed, с заданной — одно падение.
Набор обязан давать одинаковый результат на любой машине; это то же
требование герметичности, ради которого делался A33.
Добавлена автоматическая фикстура, убирающая восемь переменных *_BASE_URL на
время каждого теста. После правки прогон с заданной переменной даёт
486 passed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Отчёт benchmarks/BENCHMARK_REPORT.md открывается словами «Все метрики сняты
реальным исполнением на стенде». Для трёх строк из семи это неправда.
Сверено с диском сервера. Четыре модели существуют, их размеры совпадают с
отчётом до сотых: 17,67 / 8,37 / 4,60 / 2,33 ГБ. Трёх других нет вовсе:
deepseek-coder-v2-lite и phi-4-14b — каталоги пусты, файлов GGUF нет;
nemotron-cascade-30b не существует на сервере нигде. При этом в
benchmark_results.json DeepSeek и Phi-4 помечены COMPLETED, а у Nemotron
статус честнее, но числа при нём всё равно проставлены.
На этом построена рекомендация: DeepSeek-Coder-V2-Lite назван «лучшим
выбором для максимальной скорости» с точностью до десятой доли — по модели,
которая никогда не запускалась. Владелец собирается менять рабочую модель, и
цена такой строки — неверное решение, а не неточность в документе.
Замеры по четырём настоящим моделям признаны и переделке не подлежат;
главный вывод — Qwen2.5-Coder-14B держит качество 27B при втрое большей
скорости — остаётся в силе.
Задание вводит правило: строка появляется только при наличии пути к файлу,
размера в байтах из stat, контрольной суммы и сырых таймингов от сервера.
Модель не скачалась — раздел «не проверено» с причиной, это принимается.
Добавлены новые кандидаты, отобранные по проверенным характеристикам:
qwen2.5-coder-32b, granite4.2:8b (на диске лежит 3.2-preview, а не релиз),
nemotron-3.5-lightning, laguna-xs-2.1, lfm2.5. Отклонены с проверкой:
gpt-oss:120b (80 ГБ), Qwen3.8-Flash-Next (125B/6B, ужатое IQ1_S — 72,5 ГБ).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1. OllamaAdapter: поддержка локального инстанса по умолчанию и удаленного Ollama API
с кастомным base_url и опциональным Bearer токеном, discovery по /v1/models и /api/tags,
статус квоты «Без ограничений».
2. ClaudeAdapter: реальный health_check API probe и динамический discover_models.
3. OpenRouterAdapter: обязательные заголовки HTTP-Referer и X-OpenRouter-Title,
сбор метаданных моделей (context_length, display_name).
4. NvidiaAdapter: парсинг заголовка Retry-After и динамическая задержка при 429.
5. Экспорт лимитов: эндпоинт GET /api/quotas/export (JSON / CSV), экшен export_quotas
и кнопка выгрузки в веб-интерфейсе с маскированием секретов.
6. tests/test_api_providers_a32.py: 15 тестов, 434 passed, ruff чисто.
1. P0-1: Восстановлены все веб-обработчики в app.js (openAddAccountWizard,
handleNodeAccountChange, handleNodeModelChange, handleRefreshProviderModels,
checkUpdates), связаны с потоками startDeviceAuth и startRedirectAuth,
выбор слота обязателен и понятен пользователю.
2. P0-2: Добавлены адаптеры OpenRouter и NVIDIA NIM с поддержкой динамического
base_url, множественных аккаунтов без ограничений, GET /models и честным
отображением квот/«Н/Д».
3. P0-3: В мастере подключения локального провайдера реализована кнопка автопоиска
(discover_local_models), отображение серверов, ошибок портов и автозаполнение.
4. P0-4: Полностью удален устаревший десктопный интерфейс CustomTkinter
(router/ui/** 20 файлов, hermes_hub_app.py), зависимости customtkinter и pillow
убраны из pyproject.toml и инсталляторов, оставлен единый ярлык «Hermes Hub».
5. 418 passed, 1 skipped, 4 deselected, ruff чисто.
Реализована поддержка произвольных параметров запроса (request_options) для
локальных профилей:
1. RouterProfileConfig и ProfileViewModel дополнены полем request_options
с полной поддержкой вложенных словарей и сериализацией в YAML.
2. LocalLLMAdapter подмешивает request_options в тело POST /chat/completions.
Явные поля запроса имеют приоритет, при расхождениях пишется warning.
Изоляция провайдеров сохранена: другие адаптеры не трогают request_options.
3. Неизвестные/некорректные параметры обрабатываются чисто и классифицируются
как INVALID_REQUEST с извлечением текста ошибки сервера.
4. В веб-интерфейсе реализована карточка профиля с редактором JSON параметров,
живой валидацией синтаксиса, предпросмотром тела запроса и проверкой
подключения.
5. Отсутствие зашитых параметров enable_thinking и reasoning_effort в логике.
6. 526 passed, 3 skipped, 4 deselected, ruff чисто.
Локальная модель не закрывает задачи: Hermes сообщает о таймауте 180 секунд
после четырёх вызовов и переключается на другого провайдера.
Причина измерена, а не предположена. Служба запущена с --reasoning on и
--reasoning-budget 4096, а модель выдаёт 13,4 токена в секунду. На простой
задаче рефакторинга с лимитом 1500 токенов получено:
сгенерировано 1500 токенов за 111,6 с
рассуждений 5483 символа
ответа 0 символов
Весь лимит уходит на размышления, до ответа модель не доходит. За 180 секунд
она успевает около 2400 токенов — и это тоже одни рассуждения.
Отключение рассуждений на уровне запроса проверено и работает:
reasoning_effort: none ответ за 19,5 с
chat_template_kwargs: enable_thinking=false ответ за 11,4 с
Одиннадцать секунд вместо ста одиннадцати. Серверный флаг --reasoning off
решил бы это грубо, лишив рассуждений насовсем, поэтому владелец выбрал
гибкий путь: параметры задаёт хаб, по профилю.
Задание требует не зашивать ни enable_thinking, ни reasoning_effort: набор
ключей зависит от версии llama.cpp, и это данные конфигурации, а не
константы кода. Отдельным критерием — проверка живым запросом к серверу
владельца с замером времени до и после.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверка A37 исполнением. Защита границы рабочей области работает, обходы
через ../ и ~ в validate_path отсекаются, каталоги учётных данных закрыты.
Но в validate_command нашлась дыра ровно в том месте, ради которого guard и
делался.
Тильда и переменные окружения не раскрывались перед проверкой. Путь
"~/.hermes/agy_profiles" не считался абсолютным, склеивался с каталогом
проекта в путь с буквальным "~" внутри и признавался допустимым. Измерено:
rm -rf ~/.hermes/agy_profiles РАЗРЕШЕНО
rm -rf ~/.ssh РАЗРЕШЕНО
rm -rf $HOME/.hermes РАЗРЕШЕНО
тот же путь абсолютным отказ
тот же путь через validate_path отказ
То есть самый естественный способ написать опасную команду обходил защиту, а
абсолютный путь — нет. После правки все три отклоняются, штатное удаление
внутри проекта по-прежнему разрешено.
Добавлен тест, удерживающий это свойство.
Проверено отдельно, что защита не ломает продукт: страница, app.js, health и
snapshot отдают 200, в снапшоте 13 ролей, секретов в ответе нет, проверка
обновлений работает. 508 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Локальный кодер выдаёт 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>
Повод пришёл из разбора новостей: 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>
Владелец описал рабочую схему: 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>
Владелец заметил, что настройки 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>
У владельца на сервере два 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>
Проверка кандидата 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>
Проверка кандидата A28–A31 исполнением.
1. Дублирование ролей. RoleRegistry.migrate_legacy_roles написана верно, но
НЕ ВЫЗЫВАЛАСЬ НИОТКУДА — проверено поиском по всему коду. Вместо неё
работала «идемпотентная миграция» в router_config, дописывавшая недостающие
умолчания и не убиравшая старые роли. На конфигурации владельца интерфейс
показывал 19 агентов, причём шесть пар были неотличимы по названию:
orchestrator и manager — оба «Менеджер проекта», reviewer и code-reviewer —
оба «Ревьюер кода». Разложить аккаунты по такому списку невозможно.
Миграция подключена. Внутри неё нашлась вторая ошибка: при обходе одним
проходом пустая каноническая роль затирала цепочку, перенесённую из старой.
У владельца сработало бы именно так — в manager попал бы codex-orch вместо
выставленного им ag-orch-fallback. Старые роли теперь обрабатываются
первыми: в них и лежит настроенный порядок аккаунтов.
Проверено на живой конфигурации владельца: было 19 ролей, стало 13, все
цепочки совпадают с исходными.
2. Сохранённый workflow мигрируется вместе с ролями. Идентификаторы агентов
повторяют идентификаторы ролей, и без переименования рёбра ссылались бы на
исчезнувших агентов — «Ребро ссылается на отсутствующего агента».
3. Поиск локальных серверов моделей. Раньше адрес вводился руками. Теперь
опрашиваются известные порты на петле — Ollama 11434, LM Studio 1234,
llama.cpp 8080-8082, vLLM 8000, Jan, GPT4All, Text Generation WebUI, —
параллельно, девять портов за 1.5 с. Наружу идёт только то, что ответило;
список моделей берётся у сервера. Порт, занятый чужим сервисом, показывается
с причиной, закрытые не показываются вовсе. Действие discover_local_models.
Роль в фикстурах test_workflow_service_a30 переименована в test-developer:
«developer» — псевдоним канонической developer-1, и тест проверял бы работу
псевдонимов вместо механики workflow.
475 passed, 2 skipped; ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
В adapters/antigravity_adapter.py жил модульный _AGY_INVOCATION_LOCK, общий
для ВСЕХ профилей Antigravity. Он брался при каждом вызове, у которого есть
учётные данные, то есть при каждом рабочем. Ветка без мьютекса срабатывала
только у профиля без учётки — у вызова, который и так упадёт.
Измерено на живом адаптере: три параллельных вызова по одной секунде
занимали 3.01 с. При десяти подключённых аккаунтах одновременно работал ровно
один, чем обесценивалась вся мультиаккаунтность — то, ради чего хаб и делался.
Мьютекс охранял пустоту. Он был введён в 50fde5f со словами «guarded global
gemini:antigravity credential swap ... to eliminate concurrent subprocess
race», когда подмена учётных данных была ГЛОБАЛЬНОЙ. С тех пор она стала
попрофильной: agy_subprocess пишет в profile_dir/.gemini/oauth_creds.json, а
HOME, USERPROFILE, HOMEPATH и HOMEDRIVE подменяются на каталог профиля.
Общего состояния между профилями не осталось — проверено поиском глобальных
путей и обращений к keyring, их нет.
После снятия: те же три вызова занимают 1.00 с, и каждый идёт со своим HOME
(ag-w1, ag-w2, ag-w3) — изоляция не пострадала.
Ограничение одновременности остаётся за LeaseManager: он считает лизы по
профилю и настраивается через max_concurrency, в том числе значением 1 для
локальных моделей с --parallel 1.
Добавлен тест, удерживающий это свойство: он падает, если вызовы разных
профилей снова начнут сериализоваться. Существующий тест изоляции учётных
данных проходит без изменений.
Найдено при разборе анализа, который Antigravity провёл на сервере владельца
(hermes-muliacount). 459 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Preflight and updater tests no longer probe 8081/8082 or GitHub.
Hermetic runs fail-fast on non-loopback sockets. GUI helpers are
importable without customtkinter. CI installs the web extra, and
pytest-timeout plus a wall-clock wrapper bound the suite.
CORS был настроен как allow_origins=["*"] вместе с allow_credentials=True.
FastAPI в таком сочетании не отдаёт звёздочку, а ОТРАЖАЕТ присланный Origin
обратно. Проверено запросом к работающему хабу:
Origin: https://evil.example.com
-> HTTP 200
access-control-allow-origin: https://evil.example.com
access-control-allow-credentials: true
Опаснее всего это на localhost. get_auth_token требует токен только при
небlocalhost-привязке, то есть на 127.0.0.1 проверки нет вовсе. Значит любая
открытая рядом веб-страница могла прочитать /api/snapshot со всеми
аккаунтами, почтами и квотами и вызвать /api/action — удалить учётные
данные, переписать маршрутизацию, запустить вход OAuth. Ровно так на обеих
машинах владельца хаб и работает.
Собственному интерфейсу CORS не нужен: он отдаётся тем же сервером.
Межсайтовые запросы запрещены по умолчанию, список разрешённых источников
вынесен в настройку web_api_allowed_origins — он понадобится, когда одна
панель будет смотреть на несколько хабов.
Проверено после правки: заголовков access-control в ответе нет, браузер
такой запрос заблокирует; собственный интерфейс работает, снапшот приходит
(24 профиля, 6 ролей, индикатор «Live API»). 451 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец передал набор описаний субагентов. Ревьюер сверил каждое с кодом:
большая часть уже реализована в 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>
Владелец передал готовый дизайн: брендбук с тремя темами, макеты главного
экрана, маршрутизации и остальных разделов, логотипы. Плюс список из
двенадцати субагентов с расписанными обязанностями.
Объём не помещается в одно задание, поэтому разделено на три с явными
границами по файлам:
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>
Проверено ревьюером исполнением:
настоящий запрос к релизам коммит a1e1db7 прочитан из релиза
установлен тот же коммит «Установлена последняя сборка»
короткий SHA против полного сравнение по префиксу верное
сеть недоступна отказ, а не «обновлений нет»
предел частоты GitHub API отказ с внятной причиной
в интерфейсе отметка в шапке, сборка, время проверки
отказ в интерфейсе «Ошибка проверки», не «Актуально»
Три дефекта найдены и исправлены ревьюером, подробности в c98806b: функция
была нерабочей в вебе целиком (ответ без данных), контрольная сумма
пропускалась молча при недоступном checksums.txt, перезапуск обещался, но не
выполнялся ни на одной платформе.
Правка .gitignore (artifacts/) законна и безобидна.
450 passed, 2 skipped; ruff чисто.
Проверка 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>
Владелец обновляется вручную на трёх машинах. Механизм обновления в проекте
есть, но мёртв в каждом звене, и это проверено: веб-клиент 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>
Проверено ревьюером исполнением, а не по отчёту:
пятый аккаунт провайдера 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 чисто.