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:
parent
0c325ae2ae
commit
187f181aec
1 changed files with 174 additions and 0 deletions
174
agents/inbox/2026-08-24-A20-agy-oauth-credentials.md
Normal file
174
agents/inbox/2026-08-24-A20-agy-oauth-credentials.md
Normal 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`.
|
||||
Loading…
Reference in a new issue