docs(task): A20 — восстановить Antigravity через OAuth

Маршрутизация через Antigravity не работает ни для одного из шести
аккаунтов при полностью валидных токенах: квоты по ним приходят
настоящими через прямой HTTPS, а путь через CLI падает с
AuthExpiredError.

Причина найдена и проверена: agy читает <HOME>/.gemini/oauth_creds.json
с шестью полями, включая id_token и scope. Hub пишет свой auth.json в
другом месте и другой структурой, а id_token и scope теряет в двух
местах — oauth.py:88-92 и profile_oauth.py:241-249. Ни один профиль их
не хранит.

Проверено и не сработало: подмена HOME на каталог профиля, сборка
oauth_creds.json без id_token. Значит id_token обязателен.

Владелец решил остаться на OAuth, прямой API отклонён.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hermes Team 2026-08-23 22:36:35 +07:00
parent 0c325ae2ae
commit 187f181aec

View file

@ -0,0 +1,174 @@
# Задание A20: восстановить работу Antigravity через OAuth
## Дата поступления
2026-08-24
## База
Проверочный HEAD на момент выдачи: **`0c325ae`**.
## Ветка
`antigravity/agy-oauth-credentials`
## Приоритет
Выше всего остального. Сейчас **маршрутизация через Antigravity не работает ни для одного из шести аккаунтов** — это десять профилей из двадцати двух и основной провайдер владельца.
---
## Порядок работы с git
```
cd <каталог репозитория>; git fetch origin --prune; git status
git checkout main; git pull --ff-only origin main
git checkout -b antigravity/agy-oauth-credentials
git commit -m "..." <- сначала коммит
git push -u origin antigravity/agy-oauth-credentials
```
В конце — push и проверка `git log --oneline -1 origin/antigravity/agy-oauth-credentials`, `git status` чистый.
Прошлый раз работа была закоммичена, но не отправлена, и ветка в `origin` осталась пустой. Push без коммита и коммит без push одинаково бесполезны.
---
## Решение владельца
**Подключение остаётся через OAuth.** Переход на прямой API отклонён. Задание — починить OAuth, а не обойти его.
---
## Что установлено
Диагностика проведена целиком, повторять её не нужно.
### Симптом
```
adapter.invoke(ag-w1) -> AuthExpiredError: agy error: authentication failed or timed out
agy models -> Error: Please sign in to view available models
```
При этом **квоты по тем же аккаунтам приходят настоящими**: `ag-w2` — 37.4% недельного пула Claude/GPT, `source=provider_api`. То есть OAuth-токены живые и валидные. Не работает именно путь через CLI.
### Причина
`agy` читает учётные данные из **`<HOME>/.gemini/oauth_creds.json`**. Формат — проверен по рабочей глобальной сессии владельца:
```
access_token str
refresh_token str
scope str
token_type str
id_token str (длина ~1200, это JWT)
expiry_date int (миллисекунды)
```
Hub же пишет **`<профиль>/auth.json`** в другом месте и в другой структуре:
```
token.access_token, token.refresh_token, token.expiry,
token.expires_at, token.token_type, email, auth_method, project_id
```
Файла `oauth_creds.json` в каталогах профилей **нет ни у одного аккаунта**. Поэтому `agy`, запущенный с подменённым `HOME`, не видит входа и отвечает «please sign in».
### Чего не хватает и где это теряется
Проверено по всем шести профилям: **ни один не хранит `id_token` и `scope`**.
Теряются они в двух местах:
1. **`oauth.py:88-92`** — `refresh_access_token` возвращает только четыре поля:
```python
return {
"refresh_token": data.get("refresh_token") or refresh_token,
"access_token": data["access_token"],
"expires_at": _expires_at(data.get("expires_in")),
"token_type": data.get("token_type", "Bearer"),
}
```
`id_token` и `scope` приходят от Google **в этом же ответе** и просто отбрасываются.
2. **`profile_oauth.py:241-249`** — при первичном сохранении в `auth_data["token"]` кладутся только `access_token`, `refresh_token`, `expiry`. Ни `id_token`, ни `scope`, ни `token_type`.
Косвенное подтверждение, что поле должно быть: `profile_manager.get_profile_status` для Antigravity читает `tokens.get("id_token")` и вызывает `extract_jwt_identity` — код рассчитывает на `id_token`, которого поток никогда не сохранял.
### Что уже проверено и не сработало
Чтобы вы не повторяли:
- Запуск `agy models` с подменой `HOME`/`USERPROFILE` на каталог профиля — **не помогает**, файла с учётными данными там нет.
- Сборка `oauth_creds.json` из имеющихся полей с пустым `id_token` и подставленным `scope`**не помогает**, `agy` по-прежнему требует вход. Значит одного `access_token` недостаточно, и `id_token` скорее всего обязателен.
Правка ревьюера уже в `main`: `discover_models` теперь принимает `profile_id` и подменяет окружение, таймаут поднят с 10 до 60 секунд. Основание верное, но само по себе это симптом не лечит.
---
## P0-1. Сохранять полный набор учётных данных
1. **`oauth.py`**: `refresh_access_token` возвращает `id_token` и `scope` из ответа Google наряду с остальным. Не терять их и при повторном обновлении — если Google не вернул `id_token` в ответе на refresh, сохранять прежний, а не затирать пустым.
2. **`profile_oauth.py`**: при первичном сохранении класть в `token` весь набор — `access_token`, `refresh_token`, `id_token`, `scope`, `token_type`, `expires_at`, `expiry`.
3. Запрашивать в OAuth те же **scope**, что запрашивает сам `agy`. Если текущий набор уже, `id_token` может не прийти вовсе — сверьте со `scope` из рабочей глобальной сессии (`~/.gemini/oauth_creds.json`, поле длиной ~150 символов).
**Тест:** после прохождения OAuth профиль содержит непустые `id_token` и `scope`; повторное обновление токена их не стирает.
## P0-2. Писать `oauth_creds.json` в каталог профиля
При каждом сохранении и обновлении учётных данных Hub обязан класть в `<профиль>/.gemini/oauth_creds.json` файл ровно в формате `agy`:
- `expiry_date`**миллисекунды**, не секунды. У Hub хранится `expires_at` в секундах, умножать на 1000;
- запись атомарная, через временный файл и `os.replace`. Это уже правило проекта: `do_save_settings` так и делает после того, как потеряла атомарность при переносе;
- права на файл — как у остальных хранилищ учётных данных, секрет не должен стать доступен шире.
**Тест в песочнице:** сохранение профиля создаёт `oauth_creds.json` со всеми шестью полями; `expiry_date` в миллисекундах; обновление токена перезаписывает файл, а не плодит второй.
## P0-3. Проверить, что заработало, — исполнением
Задание принимается только с доказательством:
1. `agy models` с подменённым окружением профиля возвращает непустой список — приложить вывод;
2. настоящий вызов модели через `adapter.invoke` возвращает ответ, а не `AuthExpiredError` — приложить;
3. `route_request` для роли, ведущей на Antigravity, отрабатывает без ухода в резерв по причине авторизации.
Если после правки останется нужда в **однократном повторном входе** по каждому аккаунту (вероятно: у существующих профилей `id_token` не сохранён и взяться ему неоткуда) — **сказать об этом прямо и описать порядок для владельца**. Это законный исход: шесть аккаунтов один раз пройти мастер. Молча оставить шесть нерабочих профилей — нет.
## P1-4. Обнаружение моделей после починки
Когда `agy models` заработает, доделать то, что осталось от A18 и не было сделано:
- кэш моделей **на диске**, переживающий перезапуск;
- обновление в фоне с таймаутом; таймаут **не затирает** прежний кэш;
- **ручная кнопка обновления** — владелец должен иметь возможность попробовать снова;
- при пустом кэше — «список моделей ещё не получен», без литеральных подстановок.
Сейчас `set_model` отклоняет **любую** модель, включая настоящую, потому что сравнивать не с чем. Владелец хочет поставить двум кодерам `gemini-3.1-pro-high` и не может.
---
## Ограничения
- Ваши файлы: `oauth.py`, `router/profile_oauth.py`, `router/profile_manager.py`, `agy_subprocess.py`, `router/adapters/antigravity_adapter.py`, `model_discovery*`, `credentials.py`.
- **Учётные данные не логировать.** Ни токен, ни его часть, ни `id_token` не должны попасть в журналы, в снапшот и в веб-API. Тест на отсутствие секретов в ответе уже есть — он должен продолжать проходить.
- Десктоп и веб не ломать. На `main` сейчас 342 passed.
- Тег `v0.1.1` не создавать.
## Критерии приёмки
1. Ветка в `origin`, `git status` чист.
2. После OAuth профиль содержит непустые `id_token` и `scope`; обновление токена их не теряет; проверено тестом.
3. `oauth_creds.json` пишется в каталог профиля во всех шести полях, `expiry_date` в миллисекундах, запись атомарная; проверено тестом в песочнице.
4. **Приложен вывод `agy models`, вернувший непустой список** через профиль Hub.
5. **Приложен успешный реальный вызов модели** через `adapter.invoke`.
6. Если нужен однократный повторный вход — порядок для владельца описан в отчёте.
7. Секреты не попадают в журналы и в ответ веб-API; существующий тест на секреты проходит.
8. Кэш моделей на диске, ручное обновление, таймаут не затирает кэш.
9. `ruff check .` чисто; релизный гейт не ухудшен.
10. Отчёт: `START_HEAD`, `FINAL_HEAD`, `origin/main`, `git status`, `X passed / Y skipped / Z failed`.
## Главное
Продукт создан ради маршрутизации между аккаунтами Antigravity, и именно она сейчас не работает — при полностью валидных токенах. Всё остальное подождёт.
## Порядок сдачи
Передать точный `FINAL_COMMIT_SHA`. Сдано только после появления коммита в `origin`.