# Задание 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`.
