Работа A15 выполнена, но не закоммичена: git в его окружении был недоступен. Восстановлена ревьюером из рабочего каталога. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
12 KiB
Контракт веб-интерфейса Hermes Hub
Версия контракта: 1.0 Дата: 2026-08-23
Этот документ — единственный источник истины для двух сторон: серверной (задание A15) и клиентской (A16). Обе стороны разрабатываются параллельно и до слияния друг друга не видят.
Правило, купленное дорого: в прошлом раунде служба обнаружения моделей была написана правильно, но названа model_discovery_service.py, тогда как потребитель импортировал model_discovery. Импорт был обёрнут в except ImportError, поэтому расхождение не дало ошибки — выбор моделей просто остался пустым навсегда. Здесь то же самое повторится в виде «страница грузится, но данных нет», если стороны разойдутся хоть в одном имени.
Поэтому: любое отклонение от этого документа — дефект, даже если код работает. Меняется контракт — сначала правится документ и согласуется с обеими сторонами.
1. Стек и его обоснование
Сервер: FastAPI + uvicorn. Обе зависимости уже объявлены в pyproject.toml в группе legacy — она осталась от удалённого gui_server.py и сейчас не используется ничем. Переименовать группу в web и подключить.
Клиент: обычный JavaScript и fetch, без сборки, без npm, без фреймворка.
Обоснование, а не вкусовщина. Проект ведут агенты на трёх разных машинах; любой шаг сборки означает согласование версий Node, установку зависимостей и дрейф lock-файлов между машинами. Интерфейс и так был единственным узким местом всех прошлых раундов. Страница, которую можно открыть файлом и отладить в браузере без инструментов, снимает целый класс проблем. React в этом проекте отклонён и раньше — по той же причине.
2. Границы
Веб-интерфейс дополняет десктоп, а не заменяет его немедленно. Десктопное приложение обязано продолжать работать до тех пор, пока веб не достигнет паритета. Ничего из router/ui/** не удаляется в рамках этих заданий.
Веб-слой живёт в новом пакете:
src/antigravity_provider/router/web/
__init__.py
server.py # FastAPI-приложение и запуск uvicorn
api.py # эндпоинты
security.py # привязка к адресу, токен
static/
index.html
app.js
style.css
3. Безопасность — обязательная часть, не опция
Веб-интерфейс будет работать на сервере, доступном по сети. Требования:
- По умолчанию слушать только
127.0.0.1. Другой адрес — только явным параметром запуска, и при этом обязателен токен. - Токен передаётся заголовком
X-Hub-Token, сравнивается черезsecrets.compare_digest. При запуске без токена на не-локальном адресе сервер отказывается стартовать с внятным сообщением, а не поднимается открытым. - Секреты не покидают сервер ни при каких условиях. Проверено на живых данных: сериализованный снапшот не содержит ни
access_token, ниrefresh_token, ниapi_key, ни JWT. Это состояние обязано сохраниться — на стороне сервера нужен тест, который падает при появлении в ответе любого из этих ключей. - Почты аккаунтов в снапшоте присутствуют открытым текстом (43 вхождения на машине владельца). Это персональные данные, не учётные. Маскирование — решение владельца; по умолчанию отдаём как есть, поскольку интерфейс без идентичности аккаунта бесполезен.
- Никаких секретов в URL и параметрах запроса.
4. Эндпоинты
GET /api/snapshot
Возвращает всё состояние интерфейса одним объектом.
Формат — точный результат dataclasses.asdict(HubSnapshot) с сериализацией datetime в ISO-8601. Проверено исполнением: снапшот сериализуется одним вызовом, объём около 100 КБ.
Эталонный пример лежит рядом: docs/web-api/snapshot.example.json — снят с живой машины владельца, почты замаскированы. Клиентская сторона разрабатывается против этого файла и до готовности сервера в нём не нуждается.
Ключи верхнего уровня:
generation, seq, timestamp, profiles_by_provider, all_profiles,
readiness, agents, providers, routing, quotas, metrics, is_stale
Ответ: 200 с телом. При ошибке сбора — 503 и тело {"error": "<причина по-русски>"}.
POST /api/action
Единая точка для всех действий. Тело:
{ "action": "<имя>", "data": { ... } }
Имена действий берутся ровно из существующего _handle_action в hermes_hub_app.py. Их семнадцать:
account_details add_account agent_settings assign_role
auto_assign_all check_updates delete_credentials edit_route
oauth open_routing refresh_account refresh_all
refresh_data save_settings set_main set_orchestrator
test
Ответ:
{ "ok": true, "message": "<текст для пользователя>", "data": { ... } }
{ "ok": false, "message": "<причина отказа по-русски>" }
ok: false — это 200, а не 4xx. Отказ действия — нормальный результат, а не ошибка протокола. 4xx остаётся для неизвестного действия и непройденной авторизации.
Действия open_routing и account_details в вебе — навигация, состояние держит клиент; сервер на них отвечает ok: true без побочных эффектов.
GET /api/health
{"ok": true, "version": "<версия Hub>", "auth_flows": {...}}. Без авторизации — нужен для проверки, что сервер поднялся.
Поле auth_flows содержит информацию о доступности потоков авторизации на сервере. Формат:
{
"auth_flows": {
"openai-codex": {"supported": true, "reason": "device-code"},
"grok": {"supported": true, "reason": "device-code"},
"antigravity": {"supported": false, "reason": "Требует redirect на localhost; используйте десктоп или проброс портов"},
"claude": {"supported": false, "reason": "Требует redirect на localhost; используйте десктоп или проброс портов"}
}
}
Веб-клиент обязан проверять это поле и не показывать неработающую кнопку, а предлагать обходной путь.
5. Обновление данных
Первая версия — опрос: клиент запрашивает /api/snapshot раз в 5 секунд и при действиях пользователя.
Поле seq в снапшоте монотонно растёт. Клиент обязан игнорировать ответ с seq меньше уже применённого — иначе поздний ответ затрёт свежий. Этот дефект в десктопе уже ловили, повторять не нужно.
Server-Sent Events — следующий шаг, в эти задания не входит. Закладывать под них структуру не нужно; опрос заменяется без переделки клиента.
6. Честность данных — правило распространяется на веб целиком
Прежнее и главное правило проекта действует без исключений:
- ни одного числа, идентификатора или названия модели без измерения;
- нет данных — «Н/Д» и причина рядом, доступная пользователю. Причина уже приходит в снапшоте полем
unavailable_reason; - отличать «данных нет» от «данные ещё грузятся». Сейчас это не различается, и владелец видел пустые карточки без объяснения — квота появляется только после фонового опроса. В вебе состояние загрузки обязано выглядеть как загрузка. Для этого в объект квоты (в
quotasи связанных местах) добавлено полеis_loading: bool. Еслиis_loading == true, значит идёт опрос; еслиfalseи нет квот — значит данных действительно нет.
7. Что известно про Linux заранее
Проверено по коду, чтобы никто не выяснял это заново:
paths.pyуже кроссплатформенный: при отсутствииLOCALAPPDATAуходит в~/.hermes;- поиск CLI провайдеров уже готов:
shutil.which("agy") or shutil.which("agy.exe"); - восемь мест дублируют логику
LOCALAPPDATAв обходpaths.pyи на Linux дадут неверные пути:hermes_hub_app,router_config(две штуки),launcher_bootstrap,model_discovery_service,ui/assets,agy_subprocess.
Авторизация на headless-сервере — ограничение, которое надо признать честно, а не обойти молча:
| Провайдер | Поток | На сервере без экрана |
|---|---|---|
| OpenAI Codex | device-code | работает — код вводится с любой машины |
| Grok | device-code | работает |
| Antigravity | redirect на localhost | не работает — редирект придёт на машину пользователя, а не сервера |
| Claude | redirect на localhost | не работает |
Для двух последних интерфейс обязан сказать об этом прямо и предложить обходной путь (проброс порта по SSH либо авторизация на десктопе с переносом профиля), а не показывать неработающую кнопку.