From b7a912ea45f593c64f0c9517aaea5572f3da5458 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sun, 21 Jun 2026 15:32:48 -0700 Subject: [PATCH] fix(antigravity): bake in public OAuth client + default project fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Salvage follow-up on top of @pmos69's #29474. The PR resolved the Antigravity OAuth client purely by discovering it from an installed `agy` binary or HERMES_ANTIGRAVITY_CLIENT_ID/SECRET env vars, so users without agy installed hit a hard 'client ID not available' error. Antigravity's desktop OAuth client is a public, non-confidential installed-app client (PKCE provides the security), baked into every copy of the Antigravity CLI — same posture as the gemini-cli credentials Hermes already ships in google_oauth.py. Bake it in as the final fallback (env -> discovery -> public default) and add the public default Code Assist project as the discovery fallback, matching the reference Antigravity flow. Now consumers can authenticate directly without agy installed. --- agent/antigravity_code_assist.py | 16 ++++++-- agent/antigravity_oauth.py | 47 ++++++++++++++++++++--- tests/agent/test_antigravity_cloudcode.py | 25 +++++++++--- 3 files changed, 73 insertions(+), 15 deletions(-) diff --git a/agent/antigravity_code_assist.py b/agent/antigravity_code_assist.py index c1e9d767af4..0bdc1a0bf2e 100644 --- a/agent/antigravity_code_assist.py +++ b/agent/antigravity_code_assist.py @@ -146,10 +146,20 @@ def resolve_project_context( if env_project_id: return ProjectContext(project_id=env_project_id, source="env") info = load_code_assist(access_token) + if info.project_id: + return ProjectContext( + project_id=info.project_id, + managed_project_id=info.project_id, + source="discovered", + ) + # Discovery returned no project (common on fresh consumer accounts that + # haven't been onboarded). Fall back to the public default project so the + # call chain still succeeds — mirrors the Antigravity CLI reference flow. + from agent.antigravity_oauth import DEFAULT_PROJECT_ID return ProjectContext( - project_id=info.project_id, - managed_project_id=info.project_id, - source="discovered" if info.project_id else "unknown", + project_id=DEFAULT_PROJECT_ID, + managed_project_id=DEFAULT_PROJECT_ID, + source="default", ) diff --git a/agent/antigravity_oauth.py b/agent/antigravity_oauth.py index 0422089015e..bee75f92db2 100644 --- a/agent/antigravity_oauth.py +++ b/agent/antigravity_oauth.py @@ -41,6 +41,26 @@ ENV_CLIENT_ID = "HERMES_ANTIGRAVITY_CLIENT_ID" ENV_CLIENT_SECRET = "HERMES_ANTIGRAVITY_CLIENT_SECRET" ENV_CLI_PATH = "HERMES_ANTIGRAVITY_CLI_PATH" +# Public Antigravity CLI desktop OAuth client. Like Google's gemini-cli +# credentials (see agent/google_oauth.py), this is a DESKTOP OAuth client and +# its "secret" is not confidential — installed-app clients have no +# secret-keeping requirement (PKCE provides the security), and these creds are +# baked into every copy of the Antigravity CLI. Shipping them as a fallback +# lets users without `agy` installed authenticate directly. Split into parts +# with explicit comments per the convention in google_oauth.py. +_PUBLIC_CLIENT_ID_PROJECT_NUM = "1071006060591" +_PUBLIC_CLIENT_ID_HASH = "tmhssin2h21lcre235vtolojh4g403ep" +_PUBLIC_CLIENT_SECRET_SUFFIX = "K58FWR486LdLJ1mLB8sXC4z6qDAf" + +_DEFAULT_CLIENT_ID = ( + f"{_PUBLIC_CLIENT_ID_PROJECT_NUM}-{_PUBLIC_CLIENT_ID_HASH}" + ".apps.googleusercontent.com" +) +_DEFAULT_CLIENT_SECRET = f"GOCSPX-{_PUBLIC_CLIENT_SECRET_SUFFIX}" + +# Fallback project ID when Code Assist project discovery fails entirely. +DEFAULT_PROJECT_ID = "rising-fact-p41fc" + _CLIENT_ID_PATTERN = re.compile( r"([0-9]{8,}-[a-z0-9]{20,}\.apps\.googleusercontent\.com)" ) @@ -335,7 +355,9 @@ def _get_client_id() -> str: if env_val: return env_val discovered, _ = _discover_client_credentials() - return discovered + if discovered: + return discovered + return _DEFAULT_CLIENT_ID def _get_client_secret() -> str: @@ -343,7 +365,9 @@ def _get_client_secret() -> str: if env_val: return env_val _, discovered = _discover_client_credentials() - return discovered + if discovered: + return discovered + return _DEFAULT_CLIENT_SECRET def _iter_client_credential_candidates() -> list[Tuple[str, str]]: @@ -354,15 +378,26 @@ def _iter_client_credential_candidates() -> list[Tuple[str, str]]: _discover_client_credentials() cached = _discovered_creds_cache.get("candidates") + candidates: list[Tuple[str, str]] = [] if isinstance(cached, list): - return [ + candidates = [ (str(client_id), str(client_secret)) for client_id, client_secret in cached if client_id and client_secret ] - client_id = str(_discovered_creds_cache.get("client_id") or "") - client_secret = str(_discovered_creds_cache.get("client_secret") or "") - return [(client_id, client_secret)] if client_id and client_secret else [] + else: + client_id = str(_discovered_creds_cache.get("client_id") or "") + client_secret = str(_discovered_creds_cache.get("client_secret") or "") + if client_id and client_secret: + candidates = [(client_id, client_secret)] + + # Always include the public baked-in default as a last-resort candidate so + # users without `agy` installed can still authenticate. De-dupe in case + # discovery already surfaced the same client. + default_pair = (_DEFAULT_CLIENT_ID, _DEFAULT_CLIENT_SECRET) + if default_pair not in candidates: + candidates.append(default_pair) + return candidates def _require_client_id() -> str: diff --git a/tests/agent/test_antigravity_cloudcode.py b/tests/agent/test_antigravity_cloudcode.py index 71aabb972a1..8bdcc9a8903 100644 --- a/tests/agent/test_antigravity_cloudcode.py +++ b/tests/agent/test_antigravity_cloudcode.py @@ -102,13 +102,26 @@ class TestAntigravityCredentials: assert antigravity_oauth._get_client_id().startswith("1071006060591-") assert antigravity_oauth._get_client_secret() == fake_client_secret - def test_missing_client_credentials_raise_with_setup_hint(self): - from agent.antigravity_oauth import AntigravityOAuthError, _require_client_id + def test_missing_discovery_falls_back_to_public_default(self, monkeypatch): + # With no env override and no discoverable agy install, the public + # baked-in Antigravity desktop OAuth client is used as the floor so + # users without `agy` installed can still authenticate (PKCE makes the + # installed-app "secret" non-confidential, same as gemini-cli). + from agent import antigravity_oauth + from agent.antigravity_oauth import ( + _DEFAULT_CLIENT_ID, + _DEFAULT_CLIENT_SECRET, + _require_client_id, + ) - with pytest.raises(AntigravityOAuthError) as exc_info: - _require_client_id() - assert exc_info.value.code == "antigravity_oauth_client_id_missing" - assert "HERMES_ANTIGRAVITY_CLI_PATH" in str(exc_info.value) + monkeypatch.delenv("HERMES_ANTIGRAVITY_CLIENT_ID", raising=False) + monkeypatch.delenv("HERMES_ANTIGRAVITY_CLIENT_SECRET", raising=False) + monkeypatch.delenv("HERMES_ANTIGRAVITY_CLI_PATH", raising=False) + antigravity_oauth._discovered_creds_cache.clear() + + assert _require_client_id() == _DEFAULT_CLIENT_ID + assert antigravity_oauth._get_client_secret() == _DEFAULT_CLIENT_SECRET + assert _DEFAULT_CLIENT_ID.startswith("1071006060591-") def test_pkce_challenge_is_s256(self): import base64