hermes-hub/agents/inbox/2026-08-24-A20-agy-oauth-credentials.md
Hermes Team 187f181aec 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>
2026-08-23 22:36:35 +07:00

174 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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