From 4b97baa34bec2d83e76408ce8bda633cc4812ed7 Mon Sep 17 00:00:00 2001 From: Richard Tang Date: Fri, 20 Mar 2026 16:40:15 -0700 Subject: [PATCH] feat: native google oauth for antigravity support --- core/antigravity_auth.py | 452 +++++++++++++++++++++++ core/framework/config.py | 91 +++-- core/framework/llm/antigravity.py | 22 +- core/framework/runner/runner.py | 45 +-- core/tests/test_antigravity_eventloop.py | 6 +- quickstart.sh | 136 +------ scripts/setup_worker_model.sh | 132 +------ 7 files changed, 540 insertions(+), 344 deletions(-) create mode 100644 core/antigravity_auth.py diff --git a/core/antigravity_auth.py b/core/antigravity_auth.py new file mode 100644 index 00000000..b529ade6 --- /dev/null +++ b/core/antigravity_auth.py @@ -0,0 +1,452 @@ +#!/usr/bin/env python3 +"""Antigravity authentication CLI. + +Implements OAuth2 flow for Google's Antigravity Code Assist gateway. +Credentials are stored in ~/.hive/antigravity-accounts.json. + +Usage: + python -m antigravity_auth auth account add + python -m antigravity_auth auth account list + python -m antigravity_auth auth account remove +""" + +from __future__ import annotations + +import argparse +import json +import logging +import os +import secrets +import socket +import subprocess +import sys +import time +import urllib.parse +import urllib.request +import webbrowser +from http.server import BaseHTTPRequestHandler, HTTPServer +from pathlib import Path +from typing import Any + +logging.basicConfig(level=logging.INFO, format="%(message)s") +logger = logging.getLogger(__name__) + +# OAuth endpoints +_OAUTH_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth" +_OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token" + +# Scopes for Antigravity/Cloud Code Assist +_OAUTH_SCOPES = [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/userinfo.email", + "https://www.googleapis.com/auth/userinfo.profile", +] + +# Credentials file path in ~/.hive/ +_ACCOUNTS_FILE = Path.home() / ".hive" / "antigravity-accounts.json" + +# Default project ID +_DEFAULT_PROJECT_ID = "rising-fact-p41fc" +_DEFAULT_REDIRECT_PORT = 51121 + +# OAuth credentials fetched from the opencode-antigravity-auth project. +# This project reverse-engineered and published the public OAuth credentials +# for Google's Antigravity/Cloud Code Assist API. +# Source: https://github.com/NoeFabris/opencode-antigravity-auth +_CREDENTIALS_URL = "https://raw.githubusercontent.com/NoeFabris/opencode-antigravity-auth/dev/src/constants.ts" + +# Cached credentials fetched from public source +_cached_client_id: str | None = None +_cached_client_secret: str | None = None + + +def _fetch_credentials_from_public_source() -> tuple[str | None, str | None]: + """Fetch OAuth client ID and secret from the public npm package source on GitHub.""" + global _cached_client_id, _cached_client_secret + if _cached_client_id and _cached_client_secret: + return _cached_client_id, _cached_client_secret + + try: + req = urllib.request.Request(_CREDENTIALS_URL, headers={"User-Agent": "Hive-Antigravity-Auth/1.0"}) + with urllib.request.urlopen(req, timeout=10) as resp: + content = resp.read().decode("utf-8") + import re + id_match = re.search(r'ANTIGRAVITY_CLIENT_ID\s*=\s*"([^"]+)"', content) + secret_match = re.search(r'ANTIGRAVITY_CLIENT_SECRET\s*=\s*"([^"]+)"', content) + if id_match: + _cached_client_id = id_match.group(1) + if secret_match: + _cached_client_secret = secret_match.group(1) + return _cached_client_id, _cached_client_secret + except Exception as e: + logger.debug(f"Failed to fetch credentials from public source: {e}") + return None, None + + +def get_client_id() -> str: + """Get OAuth client ID from env, config, or public source.""" + env_id = os.environ.get("ANTIGRAVITY_CLIENT_ID") + if env_id: + return env_id + + # Try hive config + hive_cfg = Path.home() / ".hive" / "configuration.json" + if hive_cfg.exists(): + try: + with open(hive_cfg) as f: + cfg = json.load(f) + cfg_id = cfg.get("llm", {}).get("antigravity_client_id") + if cfg_id: + return cfg_id + except Exception: + pass + + # Fetch from public source + client_id, _ = _fetch_credentials_from_public_source() + if client_id: + return client_id + + raise RuntimeError("Could not obtain Antigravity OAuth client ID") + + +def get_client_secret() -> str | None: + """Get OAuth client secret from env, config, or public source.""" + secret = os.environ.get("ANTIGRAVITY_CLIENT_SECRET") + if secret: + return secret + + # Try to read from hive config + hive_cfg = Path.home() / ".hive" / "configuration.json" + if hive_cfg.exists(): + try: + with open(hive_cfg) as f: + cfg = json.load(f) + secret = cfg.get("llm", {}).get("antigravity_client_secret") + if secret: + return secret + except Exception: + pass + + # Fetch from public source (npm package on GitHub) + _, secret = _fetch_credentials_from_public_source() + return secret + + +def find_free_port() -> int: + """Find an available local port.""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("", 0)) + s.listen(1) + return s.getsockname()[1] + + +class OAuthCallbackHandler(BaseHTTPRequestHandler): + """Handle OAuth callback from browser.""" + + auth_code: str | None = None + state: str | None = None + error: str | None = None + + def log_message(self, format: str, *args: Any) -> None: + pass # Suppress default logging + + def do_GET(self) -> None: + parsed = urllib.parse.urlparse(self.path) + + if parsed.path == "/oauth-callback": + query = urllib.parse.parse_qs(parsed.query) + + if "error" in query: + self.error = query["error"][0] + self._send_response("Authentication failed. You can close this window.") + return + + if "code" in query and "state" in query: + OAuthCallbackHandler.auth_code = query["code"][0] + OAuthCallbackHandler.state = query["state"][0] + self._send_response( + "Authentication successful! You can close this window and return to the terminal." + ) + return + + self._send_response("Waiting for authentication...") + + def _send_response(self, message: str) -> None: + self.send_response(200) + self.send_header("Content-Type", "text/html") + self.end_headers() + html = f""" + +Antigravity Auth + +
+

{message}

+
+ +""" + self.wfile.write(html.encode()) + + +def wait_for_callback(port: int, timeout: int = 300) -> tuple[str | None, str | None, str | None]: + """Start local server and wait for OAuth callback.""" + server = HTTPServer(("localhost", port), OAuthCallbackHandler) + server.timeout = 1 + + start = time.time() + while time.time() - start < timeout: + if OAuthCallbackHandler.auth_code: + return ( + OAuthCallbackHandler.auth_code, + OAuthCallbackHandler.state, + OAuthCallbackHandler.error, + ) + server.handle_request() + + return None, None, "timeout" + + +def exchange_code_for_tokens( + code: str, redirect_uri: str, client_id: str, client_secret: str | None +) -> dict[str, Any] | None: + """Exchange authorization code for tokens.""" + data = { + "code": code, + "client_id": client_id, + "redirect_uri": redirect_uri, + "grant_type": "authorization_code", + } + if client_secret: + data["client_secret"] = client_secret + + body = urllib.parse.urlencode(data).encode() + + req = urllib.request.Request( + _OAUTH_TOKEN_URL, + data=body, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + method="POST", + ) + + try: + with urllib.request.urlopen(req, timeout=30) as resp: + return json.loads(resp.read()) + except Exception as e: + logger.error(f"Token exchange failed: {e}") + return None + + +def get_user_email(access_token: str) -> str | None: + """Get user email from Google API.""" + req = urllib.request.Request( + "https://www.googleapis.com/oauth2/v2/userinfo", + headers={"Authorization": f"Bearer {access_token}"}, + ) + try: + with urllib.request.urlopen(req, timeout=10) as resp: + data = json.loads(resp.read()) + return data.get("email") + except Exception: + return None + + +def load_accounts() -> dict[str, Any]: + """Load existing accounts from file.""" + if not _ACCOUNTS_FILE.exists(): + return {"schemaVersion": 4, "accounts": []} + try: + with open(_ACCOUNTS_FILE) as f: + return json.load(f) + except Exception: + return {"schemaVersion": 4, "accounts": []} + + +def save_accounts(data: dict[str, Any]) -> None: + """Save accounts to file.""" + _ACCOUNTS_FILE.parent.mkdir(parents=True, exist_ok=True) + with open(_ACCOUNTS_FILE, "w") as f: + json.dump(data, f, indent=2) + logger.info(f"Saved credentials to {_ACCOUNTS_FILE}") + + +def cmd_account_add(args: argparse.Namespace) -> int: + """Add a new Antigravity account via OAuth2.""" + client_id = get_client_id() + client_secret = get_client_secret() + + if not client_secret: + logger.warning( + "No client secret configured. Token refresh may fail.\n" + "Set ANTIGRAVITY_CLIENT_SECRET env var or add " + "'antigravity_client_secret' to ~/.hive/configuration.json" + ) + + # Use fixed port and path matching Google's expected OAuth redirect URI + port = _DEFAULT_REDIRECT_PORT + redirect_uri = f"http://localhost:{port}/oauth-callback" + + # Generate state for CSRF protection + state = secrets.token_urlsafe(16) + + # Build authorization URL + params = { + "client_id": client_id, + "redirect_uri": redirect_uri, + "response_type": "code", + "scope": " ".join(_OAUTH_SCOPES), + "state": state, + "access_type": "offline", + "prompt": "consent", + } + auth_url = f"{_OAUTH_AUTH_URL}?{urllib.parse.urlencode(params)}" + + logger.info("Opening browser for authentication...") + logger.info(f"If the browser doesn't open, visit: {auth_url}\n") + + # Open browser + webbrowser.open(auth_url) + + # Wait for callback + logger.info(f"Listening for callback on port {port}...") + code, received_state, error = wait_for_callback(port) + + if error: + logger.error(f"Authentication failed: {error}") + return 1 + + if not code: + logger.error("No authorization code received") + return 1 + + if received_state != state: + logger.error("State mismatch - possible CSRF attack") + return 1 + + # Exchange code for tokens + logger.info("Exchanging authorization code for tokens...") + tokens = exchange_code_for_tokens(code, redirect_uri, client_id, client_secret) + + if not tokens: + return 1 + + access_token = tokens.get("access_token") + refresh_token = tokens.get("refresh_token") + expires_in = tokens.get("expires_in", 3600) + + if not access_token: + logger.error("No access token in response") + return 1 + + # Get user email + email = get_user_email(access_token) + if email: + logger.info(f"Authenticated as: {email}") + + # Load existing accounts and add/update + accounts_data = load_accounts() + accounts = accounts_data.get("accounts", []) + + # Build new account entry (V4 schema) + expires_ms = int((time.time() + expires_in) * 1000) + refresh_entry = f"{refresh_token}|{_DEFAULT_PROJECT_ID}" + + new_account = { + "access": access_token, + "refresh": refresh_entry, + "expires": expires_ms, + "email": email, + "enabled": True, + } + + # Update existing account or add new one + existing_idx = next( + (i for i, a in enumerate(accounts) if a.get("email") == email), None + ) + if existing_idx is not None: + accounts[existing_idx] = new_account + logger.info(f"Updated existing account: {email}") + else: + accounts.append(new_account) + logger.info(f"Added new account: {email}") + + accounts_data["accounts"] = accounts + accounts_data["schemaVersion"] = 4 + accounts_data["last_refresh"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) + + save_accounts(accounts_data) + logger.info("\n✓ Authentication complete!") + return 0 + + +def cmd_account_list(args: argparse.Namespace) -> int: + """List all stored accounts.""" + data = load_accounts() + accounts = data.get("accounts", []) + + if not accounts: + logger.info("No accounts configured.") + logger.info(f"Run 'antigravity auth account add' to add one.") + return 0 + + logger.info("Configured accounts:\n") + for i, account in enumerate(accounts, 1): + email = account.get("email", "unknown") + enabled = "enabled" if account.get("enabled", True) else "disabled" + logger.info(f" {i}. {email} ({enabled})") + + return 0 + + +def cmd_account_remove(args: argparse.Namespace) -> int: + """Remove an account by email.""" + email = args.email + data = load_accounts() + accounts = data.get("accounts", []) + + original_len = len(accounts) + accounts = [a for a in accounts if a.get("email") != email] + + if len(accounts) == original_len: + logger.error(f"No account found with email: {email}") + return 1 + + data["accounts"] = accounts + save_accounts(data) + logger.info(f"Removed account: {email}") + return 0 + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Antigravity authentication CLI", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + subparsers = parser.add_subparsers(dest="command", help="Commands") + + # auth account add + auth_parser = subparsers.add_parser("auth", help="Authentication commands") + auth_subparsers = auth_parser.add_subparsers(dest="auth_command") + + account_parser = auth_subparsers.add_parser("account", help="Account management") + account_subparsers = account_parser.add_subparsers(dest="account_command") + + add_parser = account_subparsers.add_parser("add", help="Add a new account via OAuth2") + add_parser.set_defaults(func=cmd_account_add) + + list_parser = account_subparsers.add_parser("list", help="List configured accounts") + list_parser.set_defaults(func=cmd_account_list) + + remove_parser = account_subparsers.add_parser("remove", help="Remove an account") + remove_parser.add_argument("email", help="Email of account to remove") + remove_parser.set_defaults(func=cmd_account_remove) + + args = parser.parse_args() + + if hasattr(args, "func"): + return args.func(args) + + parser.print_help() + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/core/framework/config.py b/core/framework/config.py index dce3bb4f..f3122dfb 100644 --- a/core/framework/config.py +++ b/core/framework/config.py @@ -282,18 +282,46 @@ def get_api_key() -> str | None: return None +# OAuth credentials for Antigravity are fetched from the opencode-antigravity-auth project. +# This project reverse-engineered and published the public OAuth credentials +# for Google's Antigravity/Cloud Code Assist API. +# Source: https://github.com/NoeFabris/opencode-antigravity-auth +_ANTIGRAVITY_CREDENTIALS_URL = "https://raw.githubusercontent.com/NoeFabris/opencode-antigravity-auth/dev/src/constants.ts" +_antigravity_credentials_cache: tuple[str | None, str | None] = (None, None) + + +def _fetch_antigravity_credentials() -> tuple[str | None, str | None]: + """Fetch OAuth client ID and secret from the public npm package source on GitHub.""" + global _antigravity_credentials_cache + if _antigravity_credentials_cache[0] and _antigravity_credentials_cache[1]: + return _antigravity_credentials_cache + + import re + import urllib.request + + try: + req = urllib.request.Request(_ANTIGRAVITY_CREDENTIALS_URL, headers={"User-Agent": "Hive/1.0"}) + with urllib.request.urlopen(req, timeout=10) as resp: + content = resp.read().decode("utf-8") + id_match = re.search(r'ANTIGRAVITY_CLIENT_ID\s*=\s*"([^"]+)"', content) + secret_match = re.search(r'ANTIGRAVITY_CLIENT_SECRET\s*=\s*"([^"]+)"', content) + client_id = id_match.group(1) if id_match else None + client_secret = secret_match.group(1) if secret_match else None + if client_id and client_secret: + _antigravity_credentials_cache = (client_id, client_secret) + return client_id, client_secret + except Exception as e: + logger.debug("Failed to fetch Antigravity credentials from public source: %s", e) + return None, None + + def get_antigravity_client_id() -> str: """Return the Antigravity OAuth application client ID. Checked in order: 1. ``ANTIGRAVITY_CLIENT_ID`` environment variable 2. ``llm.antigravity_client_id`` in ~/.hive/configuration.json - (written by quickstart when Antigravity is configured) - - Falls back to the well-known application client ID registered by - Google for the Antigravity IDE — same value for every user of the - application, analogous to CLAUDE_OAUTH_CLIENT_ID / CODEX_OAUTH_CLIENT_ID - in runner.py. + 3. Fetch from public source (opencode-antigravity-auth project on GitHub) """ env = os.environ.get("ANTIGRAVITY_CLIENT_ID") if env: @@ -301,43 +329,11 @@ def get_antigravity_client_id() -> str: cfg_val = get_hive_config().get("llm", {}).get("antigravity_client_id") if cfg_val: return cfg_val - # Well-known Antigravity application client ID (public, not user-specific). - # Source: github.com/NoeFabris/opencode-antigravity-auth/blob/dev/src/constants.ts - return "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com" - - -def _read_antigravity_secret_from_npm() -> str | None: - """Read the Antigravity OAuth client secret from the globally installed npm package. - - Mirrors the bash ``_read_antigravity_creds_from_npm()`` helper so that users - who have ``opencode-antigravity-auth`` installed globally always get the secret - at runtime — regardless of whether quickstart wrote it to their config. - """ - import re as _re - import subprocess - from pathlib import Path as _Path - - candidates: list[_Path] = [] - try: - npm_root = ( - subprocess.check_output(["npm", "root", "-g"], stderr=subprocess.DEVNULL, timeout=5) - .decode() - .strip() - ) - candidates.append(_Path(npm_root) / "opencode-antigravity-auth/dist/src/constants.js") - except Exception: - pass - candidates += [ - _Path("/opt/homebrew/lib/node_modules/opencode-antigravity-auth/dist/src/constants.js"), - _Path("/usr/local/lib/node_modules/opencode-antigravity-auth/dist/src/constants.js"), - _Path("/usr/lib/node_modules/opencode-antigravity-auth/dist/src/constants.js"), - ] - for p in candidates: - if p.exists(): - m = _re.search(r'"(GOCSPX-[^"]+)"', p.read_text(errors="ignore")) - if m: - return m.group(1) - return None + # Fetch from public source + client_id, _ = _fetch_antigravity_credentials() + if client_id: + return client_id + raise RuntimeError("Could not obtain Antigravity OAuth client ID") def get_antigravity_client_secret() -> str | None: @@ -346,8 +342,7 @@ def get_antigravity_client_secret() -> str | None: Checked in order: 1. ``ANTIGRAVITY_CLIENT_SECRET`` environment variable 2. ``llm.antigravity_client_secret`` in ~/.hive/configuration.json - (written by quickstart when Antigravity is configured) - 3. Globally installed ``opencode-antigravity-auth`` npm package (runtime fallback) + 3. Fetch from public source (opencode-antigravity-auth project on GitHub) Returns None when not found — token refresh will be skipped and the caller must use whatever access token is already available. @@ -358,9 +353,9 @@ def get_antigravity_client_secret() -> str | None: cfg_val = get_hive_config().get("llm", {}).get("antigravity_client_secret") or None if cfg_val: return cfg_val - # Runtime fallback: read from globally installed npm package so users who set up - # via a path other than the updated quickstart still get the secret automatically. - return _read_antigravity_secret_from_npm() + # Fetch from public source + _, secret = _fetch_antigravity_credentials() + return secret def get_gcu_enabled() -> bool: diff --git a/core/framework/llm/antigravity.py b/core/framework/llm/antigravity.py index d862ebcf..0ecea51c 100644 --- a/core/framework/llm/antigravity.py +++ b/core/framework/llm/antigravity.py @@ -5,15 +5,11 @@ Claude, and GPT-OSS models through a single Gemini-style interface. It is NOT the public ``generativelanguage.googleapis.com`` API. Authentication uses Google OAuth2. Token refresh is done directly with the -OAuth client secret — no local proxy (``antigravity-auth serve``) required. +OAuth client secret — no local proxy required. Credential sources (checked in order): - 1. ``~/.config/opencode/antigravity-accounts.json`` (written by the - opencode-antigravity-auth plugin — V4 schema with proper expiry info) - 2. Antigravity IDE SQLite state DB (macOS / Linux) - -References: - https://github.com/NoeFabris/opencode-antigravity-auth + 1. ``~/.hive/antigravity-accounts.json`` (native OAuth implementation) + 2. Antigravity IDE SQLite state DB (macOS / Linux) """ from __future__ import annotations @@ -54,7 +50,8 @@ _ENDPOINTS = [ _DEFAULT_PROJECT_ID = "rising-fact-p41fc" _TOKEN_REFRESH_BUFFER_SECS = 60 -_ACCOUNTS_FILE = Path.home() / ".config" / "opencode" / "antigravity-accounts.json" +# Credentials file in ~/.hive/ (native implementation) +_ACCOUNTS_FILE = Path.home() / ".hive" / "antigravity-accounts.json" _IDE_STATE_DB_MAC = ( Path.home() / "Library" @@ -87,7 +84,9 @@ _BASE_HEADERS: dict[str, str] = { def _load_from_json_file() -> tuple[str | None, str | None, str, float]: - """Read credentials from ``~/.config/opencode/antigravity-accounts.json``. + """Read credentials from JSON accounts file. + + Reads from ~/.hive/antigravity-accounts.json. Returns ``(access_token | None, refresh_token | None, project_id, expires_at)``. ``expires_at`` is a Unix timestamp (seconds); 0.0 means unknown. @@ -539,9 +538,8 @@ class AntigravityProvider(LLMProvider): return self._access_token raise RuntimeError( - "No valid Antigravity credentials. Authenticate with the " - "opencode-antigravity-auth plugin: " - "https://github.com/NoeFabris/opencode-antigravity-auth" + "No valid Antigravity credentials. " + "Run: uv run python core/antigravity_auth.py auth account add" ) # --- Request building -------------------------------------------------- # diff --git a/core/framework/runner/runner.py b/core/framework/runner/runner.py index f9f41270..398fcfba 100644 --- a/core/framework/runner/runner.py +++ b/core/framework/runner/runner.py @@ -572,14 +572,10 @@ ANTIGRAVITY_IDE_STATE_DB = ( ANTIGRAVITY_IDE_STATE_DB_LINUX = ( Path.home() / ".config" / "Antigravity" / "User" / "globalStorage" / "state.vscdb" ) -# antigravity-auth CLI tool stores credentials in JSON files -ANTIGRAVITY_AUTH_FILE = Path.home() / ".config" / "opencode" / "antigravity-accounts.json" -ANTIGRAVITY_AUTH_FILE_FALLBACK = Path.home() / ".config" / "antigravity_auth" / "accounts.json" +# Antigravity credentials stored by native OAuth implementation +ANTIGRAVITY_AUTH_FILE = Path.home() / ".hive" / "antigravity-accounts.json" ANTIGRAVITY_OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token" -ANTIGRAVITY_OAUTH_CLIENT_ID = ( - "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com" -) _ANTIGRAVITY_TOKEN_LIFETIME_SECS = 3600 # Google access tokens expire in 1 hour _ANTIGRAVITY_IDE_STATE_DB_KEY = "antigravityUnifiedStateSync.oauthToken" @@ -665,8 +661,7 @@ def _read_antigravity_credentials() -> dict | None: Checks in order: 1. Antigravity IDE SQLite state database (native macOS/Linux app) - 2. antigravity-auth CLI JSON file (~/.config/opencode/antigravity-accounts.json) - 3. antigravity-auth CLI fallback (~/.config/antigravity_auth/accounts.json) + 2. Native OAuth credentials file (~/.hive/antigravity-accounts.json) Returns: Auth data dict with an ``accounts`` list on success, None otherwise. @@ -676,18 +671,16 @@ def _read_antigravity_credentials() -> dict | None: if ide_creds: return ide_creds - # 2 & 3. antigravity-auth CLI tool JSON files - for path in (ANTIGRAVITY_AUTH_FILE, ANTIGRAVITY_AUTH_FILE_FALLBACK): - if not path.exists(): - continue + # 2. Native OAuth credentials file + if ANTIGRAVITY_AUTH_FILE.exists(): try: - with open(path, encoding="utf-8") as f: + with open(ANTIGRAVITY_AUTH_FILE, encoding="utf-8") as f: data = json.load(f) accounts = data.get("accounts", []) if accounts and isinstance(accounts[0], dict): return data except (json.JSONDecodeError, OSError): - continue + pass return None @@ -717,12 +710,7 @@ def _is_antigravity_token_expired(auth_data: dict) -> bool: last_refresh_val: float | str | None = auth_data.get("last_refresh") if last_refresh_val is None: try: - path = ( - ANTIGRAVITY_AUTH_FILE - if ANTIGRAVITY_AUTH_FILE.exists() - else ANTIGRAVITY_AUTH_FILE_FALLBACK - ) - last_refresh_val = path.stat().st_mtime + last_refresh_val = ANTIGRAVITY_AUTH_FILE.stat().st_mtime except OSError: return True elif isinstance(last_refresh_val, str): @@ -751,13 +739,14 @@ def _refresh_antigravity_token(refresh_token: str) -> dict | None: import urllib.parse import urllib.request - from framework.config import get_antigravity_client_secret + from framework.config import get_antigravity_client_id, get_antigravity_client_secret + client_id = get_antigravity_client_id() client_secret = get_antigravity_client_secret() params: dict = { "grant_type": "refresh_token", "refresh_token": refresh_token, - "client_id": ANTIGRAVITY_OAUTH_CLIENT_ID, + "client_id": client_id, } if client_secret: params["client_secret"] = client_secret @@ -803,13 +792,8 @@ def _save_refreshed_antigravity_credentials(auth_data: dict, token_data: dict) - auth_data["accounts"] = accounts auth_data["last_refresh"] = datetime.now(UTC).isoformat() - target_path = ( - ANTIGRAVITY_AUTH_FILE - if ANTIGRAVITY_AUTH_FILE.exists() - else ANTIGRAVITY_AUTH_FILE_FALLBACK - ) - target_path.parent.mkdir(parents=True, exist_ok=True) - fd = os.open(target_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) + ANTIGRAVITY_AUTH_FILE.parent.mkdir(parents=True, exist_ok=True) + fd = os.open(ANTIGRAVITY_AUTH_FILE, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) with os.fdopen(fd, "w", encoding="utf-8") as f: json.dump(auth_data, f, indent=2) logger.debug("Antigravity credentials refreshed and saved") @@ -1559,8 +1543,7 @@ class AgentRunner: if not provider.has_credentials(): print( "Warning: Antigravity credentials not found. " - "Install the opencode-antigravity-auth plugin and authenticate: " - "https://github.com/NoeFabris/opencode-antigravity-auth" + "Run: uv run python core/antigravity_auth.py auth account add" ) self._llm = provider else: diff --git a/core/tests/test_antigravity_eventloop.py b/core/tests/test_antigravity_eventloop.py index 64aec3ed..4c5f7993 100644 --- a/core/tests/test_antigravity_eventloop.py +++ b/core/tests/test_antigravity_eventloop.py @@ -3,10 +3,8 @@ Run: .venv/bin/python core/tests/test_antigravity_eventloop.py Requires: - - ~/.config/opencode/antigravity-accounts.json with valid credentials - (run 'antigravity-auth accounts add' to authenticate) - - antigravity-auth serve running on localhost:8069 - (run 'antigravity-auth serve' in a separate terminal) + - ~/.hive/antigravity-accounts.json with valid credentials + (run 'uv run python core/antigravity_auth.py auth account add' to authenticate) """ import asyncio diff --git a/quickstart.sh b/quickstart.sh index 09c29476..a5dbd0bb 100755 --- a/quickstart.sh +++ b/quickstart.sh @@ -846,32 +846,6 @@ prompt_model_selection() { done } -# Read Antigravity OAuth client credentials from the installed npm package. -# Exports _AG_CLIENT_SECRET and _AG_CLIENT_ID; returns 1 if package not found. -_read_antigravity_creds_from_npm() { - local constants_js="" - local npm_root - npm_root="$(npm root -g 2>/dev/null)" && \ - [ -f "$npm_root/opencode-antigravity-auth/dist/src/constants.js" ] && \ - constants_js="$npm_root/opencode-antigravity-auth/dist/src/constants.js" - if [ -z "$constants_js" ]; then - for _candidate in \ - /opt/homebrew/lib/node_modules/opencode-antigravity-auth/dist/src/constants.js \ - /usr/local/lib/node_modules/opencode-antigravity-auth/dist/src/constants.js \ - /usr/lib/node_modules/opencode-antigravity-auth/dist/src/constants.js; do - if [ -f "$_candidate" ]; then - constants_js="$_candidate" - break - fi - done - fi - [ -z "$constants_js" ] && return 1 - _AG_CLIENT_SECRET="$(grep -o '"GOCSPX-[^"]*"' "$constants_js" 2>/dev/null | tr -d '"' | head -1)" - _AG_CLIENT_ID="$(grep -o '"[0-9]*-[a-z0-9]*\.apps\.googleusercontent\.com"' "$constants_js" 2>/dev/null | tr -d '"' | head -1)" - export _AG_CLIENT_SECRET _AG_CLIENT_ID - [ -n "$_AG_CLIENT_SECRET" ] && [ -n "$_AG_CLIENT_ID" ] -} - # Function to save configuration # Args: provider_id env_var model max_tokens max_context_tokens [use_claude_code_sub] [api_base] [use_codex_sub] [use_antigravity_sub] save_configuration() { @@ -896,11 +870,6 @@ save_configuration() { max_context_tokens=120000 fi - if _read_antigravity_creds_from_npm 2>/dev/null; then - export ANTIGRAVITY_CLIENT_SECRET="$_AG_CLIENT_SECRET" - export ANTIGRAVITY_CLIENT_ID="$_AG_CLIENT_ID" - fi - uv run python - \ "$provider_id" \ "$env_var" \ @@ -1050,10 +1019,8 @@ if [ -f "$HOME/Library/Application Support/Antigravity/User/globalStorage/state. ANTIGRAVITY_CRED_DETECTED=true elif [ -f "$HOME/.config/Antigravity/User/globalStorage/state.vscdb" ]; then ANTIGRAVITY_CRED_DETECTED=true -# Fallback: antigravity-auth CLI tool JSON files -elif [ -f "$HOME/.config/opencode/antigravity-accounts.json" ]; then - ANTIGRAVITY_CRED_DETECTED=true -elif [ -f "$HOME/.config/antigravity_auth/accounts.json" ]; then +# Native OAuth credentials +elif [ -f "$HOME/.hive/antigravity-accounts.json" ]; then ANTIGRAVITY_CRED_DETECTED=true fi @@ -1377,106 +1344,23 @@ case $choice in echo "" echo -e "${CYAN} Setting up Antigravity authentication...${NC}" echo "" - - # ── Step 1: ensure opencode is installed ──────────────────────── - if ! command -v opencode &>/dev/null; then - echo -e " Installing ${CYAN}opencode${NC} (required for Antigravity OAuth)..." - if command -v npm &>/dev/null; then - npm install -g opencode-ai || { echo -e "${RED} npm install failed. Install Node.js and retry.${NC}"; exit 1; } - else - echo -e "${RED} Node.js/npm not found. Install Node.js first: https://nodejs.org${NC}" - exit 1 - fi - fi - - # ── Step 2: write opencode.json with the plugin ───────────────── - OPENCODE_CFG_DIR="$HOME/.config/opencode" - OPENCODE_CFG="$OPENCODE_CFG_DIR/opencode.json" - mkdir -p "$OPENCODE_CFG_DIR" - - if [ ! -f "$OPENCODE_CFG" ]; then - cat > "$OPENCODE_CFG" <<'JSON' -{ - "$schema": "https://opencode.ai/config.json", - "plugin": ["opencode-antigravity-auth@latest"], - "provider": { - "google": { - "models": { - "antigravity-gemini-3-flash": { - "name": "Gemini 3 Flash (Antigravity)", - "limit": { "context": 1048576, "output": 65536 }, - "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] } - }, - "antigravity-gemini-3-pro": { - "name": "Gemini 3 Pro (Antigravity)", - "limit": { "context": 1048576, "output": 65535 }, - "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] } - }, - "antigravity-gemini-3.1-pro": { - "name": "Gemini 3.1 Pro (Antigravity)", - "limit": { "context": 1048576, "output": 65535 }, - "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] } - }, - "antigravity-claude-sonnet-4-6": { - "name": "Claude Sonnet 4.6 (Antigravity)", - "limit": { "context": 200000, "output": 64000 }, - "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] } - }, - "antigravity-claude-opus-4-6-thinking": { - "name": "Claude Opus 4.6 Thinking (Antigravity)", - "limit": { "context": 200000, "output": 64000 }, - "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] } - } - } - } - } -} -JSON - else - # Inject plugin entry if not already present - uv run python3 - "$OPENCODE_CFG" <<'PY' 2>/dev/null || true -import json, sys -p = sys.argv[1] -try: - with open(p) as f: - cfg = json.load(f) -except Exception: - cfg = {} -plugins = cfg.get("plugin", []) -entry = "opencode-antigravity-auth@latest" -if entry not in plugins: - plugins.append(entry) -cfg["plugin"] = plugins -import os -fd = os.open(p, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o644) -with os.fdopen(fd, "w") as f: - json.dump(cfg, f, indent=2) -print("plugin entry added") -PY - fi - - echo -e " ${GREEN}✓${NC} opencode.json configured with antigravity plugin" - echo "" echo -e " ${YELLOW}A browser window will open for Google OAuth.${NC}" echo -e " Sign in with your Google account that has Antigravity access." echo "" - # ── Step 3: run opencode auth login ───────────────────────────── - opencode auth login || true - - # ── Step 4: re-detect credentials ─────────────────────────────── - if [ -f "$HOME/.config/opencode/antigravity-accounts.json" ]; then - ANTIGRAVITY_CRED_DETECTED=true - elif [ -f "$HOME/.config/antigravity_auth/accounts.json" ]; then - ANTIGRAVITY_CRED_DETECTED=true + # Run native OAuth flow + if uv run python "$SCRIPT_DIR/core/antigravity_auth.py" auth account add; then + # Re-detect credentials + if [ -f "$HOME/.hive/antigravity-accounts.json" ]; then + ANTIGRAVITY_CRED_DETECTED=true + fi fi if [ "$ANTIGRAVITY_CRED_DETECTED" = false ]; then echo "" - echo -e "${RED} Authentication did not produce credentials.${NC}" - echo -e " Run ${CYAN}opencode auth login${NC} manually and try again." + echo -e "${RED} Authentication failed or was cancelled.${NC}" echo "" - exit 1 + SELECTED_PROVIDER_ID="" fi fi diff --git a/scripts/setup_worker_model.sh b/scripts/setup_worker_model.sh index e25ae097..4445c284 100755 --- a/scripts/setup_worker_model.sh +++ b/scripts/setup_worker_model.sh @@ -423,32 +423,6 @@ prompt_model_selection() { done } -# Read Antigravity OAuth client credentials from the installed npm package. -# Exports _AG_CLIENT_SECRET and _AG_CLIENT_ID; returns 1 if package not found. -_read_antigravity_creds_from_npm() { - local constants_js="" - local npm_root - npm_root="$(npm root -g 2>/dev/null)" && \ - [ -f "$npm_root/opencode-antigravity-auth/dist/src/constants.js" ] && \ - constants_js="$npm_root/opencode-antigravity-auth/dist/src/constants.js" - if [ -z "$constants_js" ]; then - for _candidate in \ - /opt/homebrew/lib/node_modules/opencode-antigravity-auth/dist/src/constants.js \ - /usr/local/lib/node_modules/opencode-antigravity-auth/dist/src/constants.js \ - /usr/lib/node_modules/opencode-antigravity-auth/dist/src/constants.js; do - if [ -f "$_candidate" ]; then - constants_js="$_candidate" - break - fi - done - fi - [ -z "$constants_js" ] && return 1 - _AG_CLIENT_SECRET="$(grep -o '"GOCSPX-[^"]*"' "$constants_js" 2>/dev/null | tr -d '"' | head -1)" - _AG_CLIENT_ID="$(grep -o '"[0-9]*-[a-z0-9]*\.apps\.googleusercontent\.com"' "$constants_js" 2>/dev/null | tr -d '"' | head -1)" - export _AG_CLIENT_SECRET _AG_CLIENT_ID - [ -n "$_AG_CLIENT_SECRET" ] && [ -n "$_AG_CLIENT_ID" ] -} - # ── Save worker_llm section to configuration.json ──────────────────── # Args: provider_id env_var model max_tokens max_context_tokens [use_claude_code_sub] [api_base] [use_codex_sub] [use_antigravity_sub] @@ -469,11 +443,6 @@ save_worker_configuration() { if [ -z "$max_tokens" ]; then max_tokens=8192; fi if [ -z "$max_context_tokens" ]; then max_context_tokens=120000; fi - if _read_antigravity_creds_from_npm 2>/dev/null; then - export ANTIGRAVITY_CLIENT_SECRET="$_AG_CLIENT_SECRET" - export ANTIGRAVITY_CLIENT_ID="$_AG_CLIENT_ID" - fi - cd "$PROJECT_DIR" uv run python - \ "$provider_id" \ @@ -646,10 +615,8 @@ if [ -f "$HOME/Library/Application Support/Antigravity/User/globalStorage/state. ANTIGRAVITY_CRED_DETECTED=true elif [ -f "$HOME/.config/Antigravity/User/globalStorage/state.vscdb" ]; then ANTIGRAVITY_CRED_DETECTED=true -# Fallback: antigravity-auth CLI tool JSON files -elif [ -f "$HOME/.config/opencode/antigravity-accounts.json" ]; then - ANTIGRAVITY_CRED_DETECTED=true -elif [ -f "$HOME/.config/antigravity_auth/accounts.json" ]; then +# Native OAuth credentials +elif [ -f "$HOME/.hive/antigravity-accounts.json" ]; then ANTIGRAVITY_CRED_DETECTED=true fi @@ -973,102 +940,21 @@ case $choice in echo "" echo -e "${CYAN} Setting up Antigravity authentication...${NC}" echo "" - - # ── Step 1: ensure opencode is installed ──────────────────────── - if ! command -v opencode &>/dev/null; then - echo -e " Installing ${CYAN}opencode${NC} (required for Antigravity OAuth)..." - if command -v npm &>/dev/null; then - npm install -g opencode-ai || { echo -e "${RED} npm install failed. Install Node.js and retry.${NC}"; exit 1; } - else - echo -e "${RED} Node.js/npm not found. Install Node.js first: https://nodejs.org${NC}" - exit 1 - fi - fi - - # ── Step 2: write opencode.json with the plugin ───────────────── - OPENCODE_CFG_DIR="$HOME/.config/opencode" - OPENCODE_CFG="$OPENCODE_CFG_DIR/opencode.json" - mkdir -p "$OPENCODE_CFG_DIR" - - if [ ! -f "$OPENCODE_CFG" ]; then - cat > "$OPENCODE_CFG" <<'JSON' -{ - "$schema": "https://opencode.ai/config.json", - "plugin": ["opencode-antigravity-auth@latest"], - "provider": { - "google": { - "models": { - "antigravity-gemini-3-flash": { - "name": "Gemini 3 Flash (Antigravity)", - "limit": { "context": 1048576, "output": 65536 }, - "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] } - }, - "antigravity-gemini-3-pro": { - "name": "Gemini 3 Pro (Antigravity)", - "limit": { "context": 1048576, "output": 65535 }, - "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] } - }, - "antigravity-gemini-3.1-pro": { - "name": "Gemini 3.1 Pro (Antigravity)", - "limit": { "context": 1048576, "output": 65535 }, - "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] } - }, - "antigravity-claude-sonnet-4-6": { - "name": "Claude Sonnet 4.6 (Antigravity)", - "limit": { "context": 200000, "output": 64000 }, - "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] } - }, - "antigravity-claude-opus-4-6-thinking": { - "name": "Claude Opus 4.6 Thinking (Antigravity)", - "limit": { "context": 200000, "output": 64000 }, - "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] } - } - } - } - } -} -JSON - else - uv run python3 - "$OPENCODE_CFG" <<'PY' 2>/dev/null || true -import json, sys -p = sys.argv[1] -try: - with open(p) as f: - cfg = json.load(f) -except Exception: - cfg = {} -plugins = cfg.get("plugin", []) -entry = "opencode-antigravity-auth@latest" -if entry not in plugins: - plugins.append(entry) -cfg["plugin"] = plugins -import os -fd = os.open(p, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o644) -with os.fdopen(fd, "w") as f: - json.dump(cfg, f, indent=2) -PY - fi - - echo -e " ${GREEN}✓${NC} opencode.json configured with antigravity plugin" - echo "" echo -e " ${YELLOW}A browser window will open for Google OAuth.${NC}" echo -e " Sign in with your Google account that has Antigravity access." echo "" - # ── Step 3: run opencode auth login ───────────────────────────── - opencode auth login || true - - # ── Step 4: re-detect credentials ─────────────────────────────── - if [ -f "$HOME/.config/opencode/antigravity-accounts.json" ]; then - ANTIGRAVITY_CRED_DETECTED=true - elif [ -f "$HOME/.config/antigravity_auth/accounts.json" ]; then - ANTIGRAVITY_CRED_DETECTED=true + # Run native OAuth flow + if uv run python "$PROJECT_DIR/core/antigravity_auth.py" auth account add; then + # Re-detect credentials + if [ -f "$HOME/.hive/antigravity-accounts.json" ]; then + ANTIGRAVITY_CRED_DETECTED=true + fi fi if [ "$ANTIGRAVITY_CRED_DETECTED" = false ]; then echo "" - echo -e "${RED} Authentication did not produce credentials.${NC}" - echo -e " Run ${CYAN}opencode auth login${NC} manually and try again." + echo -e "${RED} Authentication failed or was cancelled.${NC}" echo "" exit 1 fi