# Задание A15 (Antigravity Pro, этот ПК): веб-API и порт на Linux ## Дата поступления 2026-08-23 ## База Проверочный HEAD на момент выдачи: **`7ae4a28`**. ## Ветка `antigravity/web-api` --- ## Порядок работы с git ``` cd <каталог репозитория>; git fetch origin --prune; git status git checkout main; git pull --ff-only origin main git checkout -b antigravity/web-api ``` После первого коммита — `git push -u origin antigravity/web-api`. В конце — push и проверка `git log --oneline -1 origin/antigravity/web-api`, `git status` должен быть чистым. --- ## Что принято по A13 Проверено исполнением, работа сильная. **Устойчивость набора закрыта по-настоящему.** Общий корень Tk на сессию вместо шести отдельных. Прогнал десять полных прогонов подряд, как требовало задание: **316 passed, ноль ошибок во всех десяти**. До правки каждый прогон давал до семи ошибок, кочующих между файлами, и настоящую поломку приходилось искать перепроверкой в изоляции. Это чинилось не для галочки — оно мешало работе каждый раунд. **Раздел контракта написан по существу**: все четыре вопроса раскрыты, явно сказано, что роль Hermes не передаёт. Варианты связывания профилей поданы таблицей с оценкой 3–4 / 8–12 / 4–5 дней, ни один не реализован без решения владельца — ровно как требовалось. Одно исправлено при слиянии: в документе оказалось **11 символов BEL (0x07) на месте буквы «a»** — последовательности вида `\agy-05` были разобраны как escape. Пострадали 19 идентификаторов: `antigravity` → `ntigravity`, `ag-w2` → `g-w2`. Контракт читают оба исполнителя, битые имена в нём недопустимы. Проверяйте документы после записи так же, как код. --- ## Решение владельца: переходим на веб-интерфейс У владельца сервер с Ubuntu Server и Xubuntu, Hermes там будет линуксовый. Веб-интерфейс на сервере строго лучше десктопа: X-forwarding CustomTkinter по сети — мучение, а интерфейс и так был единственным узким местом всех прошлых раундов. Десктоп **остаётся рабочим** до достижения паритета. Ничего из `router/ui/**` не удаляется. **Читать перед началом: `docs/web-api/CONTRACT.md`.** Это единственный источник истины для вас и для A16, который параллельно делает клиентскую часть. Отклонение от контракта — дефект, даже если ваш код работает: вторая сторона пишется против документа и вашего кода не видит. --- ## P0-1. Веб-API поверх готового снапшота Архитектура уже сделала бо́льшую часть работы, проверено исполнением: `HubSnapshot` — dataclass, сериализуется в JSON **одним вызовом** `dataclasses.asdict`, объём около 100 КБ, вся поверхность действий сведена в `_handle_action` и состоит из семнадцати имён. Новый пакет `src/antigravity_provider/router/web/` — структура задана контрактом. Эндпоинты — по контракту, раздел 4: - `GET /api/snapshot` — весь снапшот, `datetime` в ISO-8601; - `POST /api/action` — тело `{"action": "...", "data": {...}}`, ответ `{"ok": bool, "message": str, "data": {...}}`. **Отказ действия — это `200` с `ok: false`**, а не `4xx`; `4xx` остаётся неизвестному действию и непройденной авторизации; - `GET /api/health` — без авторизации. Стек: FastAPI и uvicorn. Обе зависимости **уже объявлены** в `pyproject.toml` в группе `legacy` — она осталась от удалённого `gui_server.py` и не используется ничем. Переименовать в `web` и подключить. Действия исполнять **через существующий путь**, а не дублировать логику. `_handle_action` сейчас завязан на виджеты; вынести из него исполнительную часть так, чтобы её вызывали и десктоп, и веб. Второй реализации семнадцати действий в проекте быть не должно. **Долгие операции не должны держать запрос.** `refresh_all` опрашивает провайдеров по сети, `test` дёргает подпроцесс, `agy models` в замерах то отвечает за 40 секунд, то висит больше двух минут. Такие действия возвращают `ok: true` с сообщением «запущено», а результат приходит следующим снапшотом. ## P0-2. Безопасность — часть задания, не довесок Сервер будет доступен по сети. Контракт, раздел 3: 1. По умолчанию слушать **только `127.0.0.1`**. Другой адрес — явным параметром, и тогда **токен обязателен**. 2. Токен в заголовке `X-Hub-Token`, сравнение через `secrets.compare_digest`. 3. При запуске на не-локальном адресе без токена сервер **отказывается стартовать** с внятным сообщением. Не поднимается открытым, не пишет предупреждение в лог и не продолжает. 4. **Тест, падающий при появлении секрета в ответе.** Проверено на живых данных: сейчас в сериализованном снапшоте нет ни `access_token`, ни `refresh_token`, ни `api_key`, ни JWT, ни строк `ya29.`/`sk-`. Это состояние надо удержать механически, а не обещанием. 5. Никаких секретов в URL и параметрах запроса. ## P0-3. Порт на Linux Проверено по коду — ядро почти готово: - `paths.py` **уже** кроссплатформенный: при отсутствии `LOCALAPPDATA` уходит в `~/.hermes`; - поиск CLI **уже** готов: `shutil.which("agy") or shutil.which("agy.exe")`. Чинить надо восемь мест, которые дублируют логику `LOCALAPPDATA` **в обход** `paths.py` и на Linux дадут неверные пути: ``` router/hermes_hub_app.py:37 router/router_config.py:319, 462 router/launcher_bootstrap.py:32 router/model_discovery_service.py:34 router/ui/assets.py:48 agy_subprocess.py:50 ``` Все — через `paths.get_hermes_home()`. Единый источник истины уже есть, им просто не пользуются. **Тест:** при заданном `HERMES_HOME` ни один модуль не обращается к `LOCALAPPDATA` напрямую; пути одинаковы во всех модулях. **Проверка на настоящем Linux обязательна** — заявления «должно работать» не принимаются. Если под рукой нет машины, скажите об этом прямо в отчёте, и проверку сделает владелец. ## P0-4. Авторизация на сервере без экрана — сказать правду Разобрано по коду, выяснять заново не нужно: | Провайдер | Поток | На headless-сервере | |---|---|---| | OpenAI Codex | device-code (12 упоминаний) | **работает** | | Grok | device-code (13) | **работает** | | Antigravity | redirect на localhost | **не работает** | | Claude | redirect на localhost | **не работает** | Для Antigravity и Claude редирект придёт на машину пользователя, а не сервера, — поток обрывается. **Требуется:** серверная часть сообщает клиенту, какие потоки на этой машине доступны, а какие нет, **и почему**. Поле в ответе `/api/health` или отдельный эндпоинт — на ваше усмотрение, но зафиксируйте в контракте и предупредите A16. Обходной путь предложить, а не изобретать молча: проброс порта по SSH либо авторизация на десктопе с переносом каталога профиля. Что из этого работает — проверить и написать. **Неработающую кнопку показывать нельзя.** Это прямое продолжение правила честности: интерфейс, предлагающий подключить Antigravity на сервере, где это невозможно, — та же ложь, что выдуманная квота. ## P1-5. Долг из прошлого раунда: квота не видна до фонового опроса Найдено при проверке A14 и относится к вашей зоне. `state_store` наполняет снапшот через `quota_service.get_snapshot`, который читает кэш и **при промахе отдаёт пустую заглушку из двух корзин, живой опрос не запуская**. Замер: ``` снапшот сразу после старта : 2 корзины, 0 измеренных после прогрева кэша : 4 корзины, все измерены, ag-w2 = 37.4% ``` В работающем приложении квота появляется только после фонового обновления, а до него карточки стоят пустыми **без объяснения**. Это и есть жалоба владельца «в аккаунтах квота так и не отображается». Требуется различать в модели данных **«данных нет»** и **«данные ещё грузятся»**. Сейчас оба состояния выглядят одинаково, и ни интерфейс десктопа, ни будущий веб отличить их не могут. Поле состояния — в снапшот и в контракт. --- ## Ограничения - Параллельно идёт **A16** (клиентская часть). Ваши файлы: `router/web/**` кроме `static/`, `state_store.py`, `paths.py`, `router_config.py`, `agy_subprocess.py`, `launcher_bootstrap.py`, `model_discovery_service.py`, `pyproject.toml`, `docs/web-api/CONTRACT.md`. **Не ваши:** `router/web/static/**`, `router/ui/**`. - Контракт менять можно, но **только правкой документа с явным упоминанием в отчёте** — вторая сторона пишется против него. - Десктоп не ломать: он остаётся рабочим до паритета. - Никаких чисел и идентификаторов без измерения. - Тег `v0.1.1` не создавать. ## Критерии приёмки 1. Ветка в `origin`, финальный коммит виден, `git status` чист. 2. `GET /api/snapshot` отдаёт снапшот, совпадающий по структуре с `docs/web-api/snapshot.example.json`; проверено тестом сравнения ключей. 3. `POST /api/action` принимает все семнадцать действий; отказ возвращается как `200` с `ok: false`; неизвестное действие — `4xx`. 4. Действия исполняются через общий путь; второй реализации семнадцати действий в проекте нет. 5. Долгие операции не держат запрос; результат приходит следующим снапшотом. 6. Сервер по умолчанию слушает `127.0.0.1`; на внешнем адресе без токена **отказывается стартовать**; проверено тестом. 7. Есть тест, падающий при появлении в ответе `access_token`, `refresh_token`, `api_key`, JWT или строк `ya29.` / `sk-`. 8. Ни один модуль не читает `LOCALAPPDATA` в обход `paths.py`; проверено тестом с заданным `HERMES_HOME`. 9. Доступность потоков авторизации сообщается клиенту с причиной; зафиксировано в контракте. 10. В снапшоте различаются «данных нет» и «данные грузятся»; поле описано в контракте. 11. Прогон в обоих окружениях; `ruff check .` чисто; релизный гейт остаётся 7/7. 12. Отчёт: `START_HEAD`, `FINAL_HEAD`, `origin/main`, `git status`, точный `X passed / Y skipped / Z failed`, и отдельно — проверялось ли на настоящем Linux. ## Порядок сдачи Передать точный `FINAL_COMMIT_SHA`. Сдано только после появления коммита в `origin`.