Commit graph

307 commits

Author SHA1 Message Date
Hermes Team
a677449b9b merge: A39 поверх A34 — файлы десктопа остаются удалёнными 2026-08-31 01:33:05 +07:00
Hermes Team
1d26608887 Merge remote-tracking branch 'origin/review/a35-a37-verified' into HEAD 2026-08-31 01:27:56 +07:00
Hermes Team
1d6ef1bfcb docs(agents): задание A40 — повторный замер локальных моделей, три строки прошлого отчёта выдуманы
Отчёт 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>
2026-08-31 01:25:19 +07:00
ochenstarik-ui
ad07425c06 feat(providers): A32 интеграция Ollama с API, Claude probe, OpenRouter headers/metadata, NVIDIA Retry-After и экспорт лимитов
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 чисто.
2026-08-31 01:15:10 +07:00
ochenstarik-ui
c7539b36d4 feat(hub): A34 восстановление подключения аккаунтов, OpenRouter, NVIDIA, удаление десктопа
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 чисто.
2026-08-31 00:23:27 +07:00
ochenstarik-ui
5ac01e0a15 feat(local): параметры запроса (request_options) для локальных профилей
Реализована поддержка произвольных параметров запроса (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 чисто.
2026-08-30 23:49:57 +07:00
Hermes Team
f8b6783641 docs(agents): задание A39 — параметры запроса для локальных профилей
Локальная модель не закрывает задачи: 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>
2026-08-30 23:04:18 +07:00
Hermes Team
88d579af08 fix(security): команда с тильдой обходила защиту каталога учётных данных
Проверка 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>
2026-08-30 22:51:17 +07:00
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