133 lines
6.1 KiB
Python
133 lines
6.1 KiB
Python
"""Detect Google Antigravity (``agy``) OAuth credentials for ``antigravity-native``.
|
|
|
|
The ``agy`` CLI authenticates via a browser OAuth flow on first interactive run
|
|
— it has no ``agy login`` / ``agy auth status`` subcommand. *Where* and *how* it
|
|
persists the resulting token is **platform-specific** (verified live against agy
|
|
1.0.10):
|
|
|
|
- macOS writes ``~/.gemini/oauth_creds.json`` — a flat OAuth2 object::
|
|
|
|
{"access_token": "ya29.…", "refresh_token": "1//0g…",
|
|
"expiry_date": …, "id_token": "eyJ…", "scope": "…", "token_type": "Bearer"}
|
|
|
|
- Linux writes ``~/.gemini/antigravity-cli/antigravity-oauth-token`` — the OAuth
|
|
object **nested under** ``token``::
|
|
|
|
{"auth_method": "oauth",
|
|
"token": {"access_token": "ya29.…", "refresh_token": "1//0g…",
|
|
"token_type": "Bearer", "expiry": "…"}}
|
|
|
|
Detection is file-based and subprocess-free: it checks **both** locations and
|
|
treats a non-empty ``access_token`` / ``refresh_token`` string — flat (macOS) or
|
|
nested under ``token`` (Linux) — as a completed login, so a logged-in user is
|
|
recognized on either platform. Like
|
|
:func:`omnigent.onboarding.ambient.codex_auth_has_credential` and
|
|
:func:`omnigent.onboarding.ambient.claude_auth_has_credential`, it cannot detect
|
|
server-side revocation — its only job is to reject the "no-credential /
|
|
file-empty / file-corrupt" cases. The readiness layer uses this as a fast path;
|
|
when a caller needs a live, revocation-aware verdict (robust to any future
|
|
change of the token path/shape)
|
|
:func:`omnigent.onboarding.harness_install.harness_cli_logged_in` asks the CLI
|
|
itself by running ``agy models`` (exit 0 only when signed in). On the rare
|
|
machine carrying *both* files — e.g. a home directory migrated across OSes — a
|
|
stale token could read as logged-in; that is benign (agy re-drives OAuth on
|
|
launch) and ``agy models`` is the authoritative check.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
|
|
_GEMINI_DIR: Path = Path(os.path.expanduser("~")) / ".gemini"
|
|
|
|
# Where ``agy`` writes its OAuth token after the first interactive sign-in. The
|
|
# location is platform-specific (verified against agy 1.0.10), so detection
|
|
# checks both: macOS uses ``oauth_creds.json``; Linux uses
|
|
# ``antigravity-cli/antigravity-oauth-token``.
|
|
DEFAULT_GEMINI_OAUTH_CREDS: Path = _GEMINI_DIR / "oauth_creds.json"
|
|
LINUX_GEMINI_OAUTH_TOKEN: Path = _GEMINI_DIR / "antigravity-cli" / "antigravity-oauth-token"
|
|
GEMINI_OAUTH_CRED_PATHS: tuple[Path, ...] = (DEFAULT_GEMINI_OAUTH_CREDS, LINUX_GEMINI_OAUTH_TOKEN)
|
|
|
|
# The OAuth fields whose non-empty presence proves a completed sign-in. agy
|
|
# stores them flat (macOS) or nested under ``token`` (Linux); detection looks in
|
|
# both places.
|
|
_OAUTH_CRED_FIELDS: tuple[str, ...] = ("access_token", "refresh_token")
|
|
|
|
|
|
def _file_carries_token(path: Path) -> bool:
|
|
"""Return whether a single creds file parses as JSON carrying a usable token.
|
|
|
|
Accepts both agy token shapes: the OAuth fields flat at the top level
|
|
(macOS) or nested under a ``token`` object (Linux).
|
|
|
|
:param path: Path to a candidate credential file.
|
|
:returns: ``True`` when *path* reads as a JSON object with a non-empty
|
|
``access_token`` / ``refresh_token`` string (flat or under ``token``);
|
|
``False`` when missing, unreadable, non-UTF-8, not JSON, not an object,
|
|
or token-less.
|
|
"""
|
|
try:
|
|
raw = path.read_text(encoding="utf-8")
|
|
except (OSError, UnicodeDecodeError):
|
|
# OSError: missing / unreadable file. UnicodeDecodeError: the file
|
|
# exists but holds non-UTF-8 bytes (a corrupt creds file) — treat it as
|
|
# "no usable credential" rather than letting the decode crash.
|
|
return False
|
|
try:
|
|
data = json.loads(raw)
|
|
except json.JSONDecodeError:
|
|
return False
|
|
if not isinstance(data, dict):
|
|
return False
|
|
# macOS stores the OAuth fields flat; Linux nests them under ``token``.
|
|
nested = data.get("token")
|
|
sources = [data, nested] if isinstance(nested, dict) else [data]
|
|
for source in sources:
|
|
for field in _OAUTH_CRED_FIELDS:
|
|
value = source.get(field)
|
|
if isinstance(value, str) and value.strip():
|
|
return True
|
|
return False
|
|
|
|
|
|
def gemini_auth_has_credential(creds_path: Path | None = None) -> bool:
|
|
"""Return whether ``agy`` has a usable OAuth login on this machine.
|
|
|
|
With *creds_path* unset, checks every known platform location
|
|
(:data:`GEMINI_OAUTH_CRED_PATHS`) and returns ``True`` if any carries a
|
|
usable token — so a logged-in user is recognized on both macOS
|
|
(``oauth_creds.json``) and Linux
|
|
(``antigravity-cli/antigravity-oauth-token``). With *creds_path* set, checks
|
|
only that file.
|
|
|
|
A file counts as a completed login when it parses as a JSON object with a
|
|
non-empty ``access_token`` / ``refresh_token`` string, flat or nested under
|
|
``token``. This cannot detect server-side revocation — for that, see
|
|
:func:`omnigent.onboarding.harness_install.harness_cli_logged_in`.
|
|
|
|
:param creds_path: A specific credential file to check; ``None`` checks all
|
|
of :data:`GEMINI_OAUTH_CRED_PATHS`.
|
|
:returns: ``True`` when a usable Gemini credential is present; ``False``
|
|
when every checked file is missing, unreadable, non-UTF-8, not JSON, not
|
|
an object, or token-less.
|
|
"""
|
|
paths = (creds_path,) if creds_path is not None else GEMINI_OAUTH_CRED_PATHS
|
|
return any(_file_carries_token(path) for path in paths)
|
|
|
|
|
|
def gemini_login_detected() -> bool:
|
|
"""Return whether a usable ``agy`` OAuth credential is present on this machine.
|
|
|
|
Thin wrapper over :func:`gemini_auth_has_credential` across all known
|
|
platform locations (:data:`GEMINI_OAUTH_CRED_PATHS`). Used by the readiness
|
|
layer to check whether the ``antigravity-native`` harness has a Google
|
|
subscription credential without spawning any subprocess.
|
|
|
|
:returns: ``True`` when agy's token (macOS ``~/.gemini/oauth_creds.json`` or
|
|
Linux ``~/.gemini/antigravity-cli/antigravity-oauth-token``) carries a
|
|
usable credential; ``False`` otherwise.
|
|
"""
|
|
return gemini_auth_has_credential()
|