# Задание 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` читает учётные данные из **`/.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`.