Решение владельца: у него сервер с Ubuntu Server и Xubuntu, Hermes там будет линуксовый. Веб-интерфейс на сервере строго лучше десктопа. Десктоп остаётся рабочим до паритета, router/ui/** не трогается. A15 (Pro): веб-API поверх готового снапшота — GET /api/snapshot и POST /api/action на семнадцать существующих действий; безопасность (127.0.0.1 по умолчанию, отказ стартовать на внешнем адресе без токена, тест на отсутствие секретов в ответе); порт на Linux — восемь мест, читающих LOCALAPPDATA в обход paths.py; честное сообщение о том, какие потоки авторизации на headless-сервере не работают. Плюс долг из прошлого раунда: снапшот не различает «данных нет» и «данные грузятся». A16 (Flash): клиент без сборки на обычном JS. Не заблокирован сервером — разрабатывает против docs/web-api/snapshot.example.json. Экраны по ценности: Аккаунты с видимыми квотами, затем Обзор и Маршрутизация. Обе стороны пишутся против docs/web-api/CONTRACT.md и до слияния друг друга не видят — отсюда требование не менять контракт односторонне. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
160 lines
15 KiB
Markdown
160 lines
15 KiB
Markdown
# Задание 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`.
|