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