Проверялся ровно один путь — $HERMES_HOME/config.yaml. Hermes хранит
конфигурацию по-разному в зависимости от версии и способа установки, и у
владельца на Linux хаб писал «В Hermes: конфигурация не найдена» при
работающем Hermes v0.20.6.
Теперь проверяются семь известных мест, и в сообщении перечисляется, где
именно искали, — владелец видит список и может назвать верный путь.
Отдельно различаются «файла нет» и «нет доступа к каталогу»: закрытый
правами каталог помечается как непроверенный, а не как отсутствующий.
Это тот же класс ошибки, на котором я сам дважды ошибся в диагнозе,
приняв отказ в доступе за отсутствие файла.
599 passed, ruff clean, релизный гейт 10/10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Кэш опознания. _identities и _snapshots в AccountQuotaService живут в
памяти по ключу «провайдер:слот» и при удалении ключа не чистились.
Владелец удалил аккаунт Antigravity, завёл victor.trushenko@gmail.com, а
в списке остался прежний trushenko.semya@gmail.com. Добавлен
forget_profile, вызывается при удалении учётных данных и при перезаписи
слота другим аккаунтом. Проверено: после сброса запись исчезает.
NVIDIA. Проверка подключения получала каталог моделей успешно — то есть
ключ рабочий и аккаунт опознан, — а затем делала пробный запрос к первой
чат-модели каталога. Каталог NVIDIA общий, доступ к конкретной модели
даётся по аккаунту, и ответ «Function ... Not found for account <id>»
объявлялся провалом подключения. Теперь успешный список моделей считается
доказательством работоспособности ключа, а неудачная проба возвращается
примечанием с предложением выбрать доступную модель.
401 и 403 при этом по-прежнему означают отказ: ключ отвергнут. Различие
поймал тест A54 на неверном ключе.
Выход из программы. Неудачная остановка процессов отменяла выход целиком:
владелец нажимал «закрыть» и оставался в работающем приложении. Теперь
показывается предупреждение, а программа закрывается.
599 passed, ruff clean, релизный гейт 10/10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Версия. В index.html номер стоял руками в двух местах, а в app.js был
запасным значением '0.1.1'. Номер сборки приходил из API и обновлялся,
версия — нет: владелец обновился до e6eab12 и увидел v0.1.1. Подъём
версии в пяти местах бэкенда до экрана не доходил вовсе. Теперь версия
берётся только из API; если сервер её не передал, пишется Н/Д с причиной,
а не правдоподобный номер.
Установка. Отказ остановки прежнего хаба прерывал установку целиком, и
владелец получал голый код 15. Теперь неудачная остановка не отменяет
установку: причина показывается, работа продолжается, и если файл
действительно занят, об этом скажет копирование с именем файла.
Копирование файлов получило повтор: процесс мог не успеть отпустить файл
после остановки. Пять попыток с паузой вместо отказа с первой.
599 passed, ruff clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец просил менять версию сборки. Прежнее указание держать 0.1.1
отменено этим.
Поднято во всех пяти местах, где версия объявлена: version.py,
pyproject.toml, HermesHubSetup.cs, install-linux.sh и
config/compatibility.json. Последний ловится тестом на совпадение с
__version__ — без него прогон падал.
599 passed, ruff clean, релизный гейт 10/10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверено ревьюером исполнением:
check_account больше не отказывает из-за незапущенной фоновой службы.
Раньше кнопка «Проверить подключение» перекладывала работу на службу и
возвращала «Фоновая служба проверки не запущена». Теперь выполняется
настоящий запрос, и владелец видит причину провайдера: «Не удалось
подключиться к локальному серверу LLM».
Удаление ключа больше не ждёт полного обхода провайдеров: замерено 0,0 с
против прежних тридцати.
Пустой ключ отклоняется ДО создания слота — профилей-пустышек не остаётся.
Действие clear_accounts: предпросмотр целей, подтверждение, и Antigravity
защищён — проверено, ключ ag-1 после очистки цел, в цели не попадает.
Лаунчер: значок в области уведомлений, при закрытии окна вопрос «Закрыть
Hermes Hub полностью?», выход снимает браузер и останавливает
собственный процесс сервера (StopOwnedRuntime).
Правка ревьюера: поддержка провайдера проверяется ДО требования ключа.
A54 поставил проверку ключа первой, и у неподдерживаемого провайдера
выводилось «не указан API-ключ» вместо «не поддерживается» — ключ там не
поможет, и сообщение уводило не туда. Тест A42 это поймал.
599 passed, ruff clean, релизный гейт 10/10 на обеих конфигурациях.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Хаб — оконное приложение без консоли, поэтому каждый запуск консольного
exe открывал отдельное окно. Пока проверка аккаунтов шла по нажатию, это
было незаметно. A50 сделал проверку автоматической раз в минуту, и окна
agy.exe стали появляться постоянно, мешая работе.
Добавлен hidden_process_kwargs(): CREATE_NO_WINDOW плюс STARTUPINFO с
SW_HIDE, на не-Windows пусто. Применён ко всем ФОНОВЫМ вызовам:
опрос моделей agy, выполнение запроса agy, чтение ключей, codex_oauth,
launcher_bootstrap, git rev-parse и проверка версии в обновлении.
Вызовы, где окно нужно видимым, не тронуты: вход по OAuth сознательно
использует CREATE_NEW_CONSOLE, запуск установщика тоже должен быть виден.
574 passed, ruff clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Сравнение сборок шло только на равенство коммитов: любое расхождение
объявлялось обновлением. Последний релиз на GitHub указывал на 380c218
от 30 августа, у владельца стояла сборка новее — хаб предложил «обновиться»
на старый коммит, владелец нажал и получил откат.
Добавлена проверка по времени: если релиз опубликован раньше, чем
установлена текущая сборка (deployed_at из deployment_manifest.json),
это откат, а не обновление. Такой релиз не предлагается, и владельцу
пишется, что установлена сборка новее опубликованного релиза.
Проверено на фактических данных владельца: релиз 380c218 от 2026-08-30
против установки от 2026-08-31 даёт update_available=False.
Версия остаётся 0.1.1 намеренно, сборки различаются коммитом.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверено ревьюером исполнением: подключение с выбором роли кладёт
аккаунт в её цепочку (developer-1 → ['openrouter-1']). Раньше выбор роли
до цепочки не доходил, и ни один аккаунт владельца ни в одной цепочке не
состоял.
Роль на карточке берётся из живой цепочки: role_assignments.get(pid, [])
вместо подстановки догадки. Таблица DEFAULT_SLOT_ROLES удалена — ссылок
на неё в дереве не осталось.
На маршрутизации показывается «Сейчас ответит: <аккаунт>» либо «никто»
с причиной. В плагине Hermes учитываются вызовы мимо хаба
(record_bypass в четырёх местах, счётчик bypassed_calls_count).
Обратное чтение сделано честно: get_hermes_config_status читает
конфигурацию Hermes ТОЛЬКО на чтение, с отдельным тестом на это.
Разрешение конфликтов с A49 и A50 (ветка A51 отведена от 17b368a):
- action_handler: взят вариант A50 — он надмножество, содержит и
назначение в роль, и защиту слота от чужого провайдера;
- auto_assigner: взят вариант A51 — таблица-догадка удалена;
подпись llama.cpp от A50 сохранена, она вне конфликта;
- settings_service, unified_health, app.js: обе стороны, правки
дополняют друг друга;
- тест свежести памяти: взята строгая проверка, вариант A51 молча
пропускал отсутствие записанного коммита.
Две правки ревьюера по итогам слияния:
- в app.js при сложении потерялась закрывающая скобка блока настроек;
- в add_account сохранение учётных данных вызывалось дважды, и второй
вызов обращался к auth_data, которой у уже авторизованного аккаунта не
существует. Перевод аккаунта в другую роль падал с ошибкой, хотя ключ
вводить не требуется. Дубль убран, оба теста A51 проходят.
554 passed, 1 skipped, ruff clean, релизный гейт 10/10 и на конфигурации
владельца, и на пустой.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверено ревьюером исполнением, все восемь замечаний владельца закрыты:
Чужой слот. Раньше add_account с profile_id=ag-w1 для nvidia возвращал
ok=True и клал аккаунт в слот Antigravity. Теперь отказ с причиной:
«Слот ag-w1 не принадлежит провайдеру nvidia». Свой слот принимается,
идентификатор выдаётся верный.
Группировка по провайдерам восстановлена: скрытие заголовков групп,
добавленное в A48 (display:contents + display:none), убрано.
Автоматическая проверка запускается при старте веб-сервера
(_start_background_refresh), состояние и списки моделей больше не ждут
ручного нажатия.
Облачные модели Ollama: эндпоинт не выдуман — ревьюер проверил запросом,
https://ollama.com/api/tags отвечает 200 и отдаёт 19 моделей
(gpt-oss:20b, kimi-k2.6, glm-5.1 и другие).
Локальный провайдер подписан llama.cpp вместо «Локальный сервер».
Показ хода при долгом опросе честный: «может занять до минуты на этап».
Конфликты с A49 разрешены сложением: правки дополняют друг друга —
поле пути к хранилищу Obsidian и поле периода проверки аккаунтов,
стили вкладки скиллов и стили групп провайдеров. В тесте свежести памяти
взят вариант A50: коммит извлекается из файла, а не зашит.
545 passed, 1 skipped, ruff clean, релизный гейт 10/10 и на конфигурации
владельца, и на пустой.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A48 (Codex): вёрстка по макетам. style.css изменён на 178 строк, добавлен
workspace.js, приложены 79 скриншотов — до, после, макеты для сверки.
Проверено глазами: логотип, шапка с поиском, панель инструментов холста,
карточки узлов с моделью и аккаунтом, читаемые подписи связей, инспектор с
вкладками, три карточки внизу. Выдуманных чисел из макета нет, пустые
состояния честные.
A47: хранилище AI-Memory под git локально, память проекта приведена к
действительности, worklog заполнен, составлен перечень агентов сервера.
A45: три кандидата замерены. Файлы и размеры сверены по диску, контрольная
сумма Qwen3-Coder пересчитана независимо и совпала. Скорость 109,6 ток/с
на 64К независимо НЕ перемерена: свободно 1,9 ГБ видеопамяти, проверка
потребовала бы остановить рабочий кодер владельца.
A42: подключение OpenRouter, NVIDIA и Ollama, отдельные ветки обнаружения
моделей, отказ с причиной вместо мнимого успеха.
Конфликт A42 с A41 разрешён в пользу A41: ветка A42 отведена от e7194d3,
до слияния A41 в main, поэтому в ней не было динамических префиксов.
Сохранены _get_prefix и генерация слотов; зашитый provider_slots не взят.
Расширенный список ролей openrouter из A42 принят.
Ожидание теста A42 поправлено: nvidia и nvidia-nim — псевдонимы одного
провайдера с одним адаптером, слоты у них общие.
517 passed, ruff clean, релизный гейт 10/10 и на конфигурации владельца,
и на пустой.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1. P0-1: В action_handler.py в add_account реализовано реальное сохранение профилей
и учетных данных для openrouter, nvidia, nvidia-nim, ollama, local, claude, opencode-go.
Для некорректных действий возвращается честная ошибка ok: False вместо мнимого успеха.
В AutoAssigner добавлены слоты и возможности для openrouter и nvidia.
2. P0-2: В ModelDiscoveryService убраны зашитые списки PID. Добавлены ветки
openrouter, nvidia и выделенная ветка ollama (/api/tags и /v1/models). Ошибки серверов
сохраняются и передаются в интерфейс.
3. P0-3: В unified_health.py разделены статусы временного отката ошибки (STATUS_COOLDOWN)
и исчерпания квоты (STATUS_QUOTA_EXHAUSTED). RATE_LIMITED проверяется до кулдаунов.
В health_tracker.py исключена пометка всего аккаунта при пустом model_name.
В codex_adapter.py уточнена классификация ошибок.
4. tests/test_a42_provider_connect.py: 15 тестов, 500 passed, ruff чисто.
Экран маршрутизации брал профили из 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 чисто.
Брендбук требует три темы, причём 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>
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 чисто.
Проверка 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>
Проверка кандидата 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>