fix(antigravity): bake in public OAuth client + default project fallback

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.
This commit is contained in:
Teknium 2026-06-21 15:32:48 -07:00
parent 8baa4e9976
commit b7a912ea45
3 changed files with 73 additions and 15 deletions

View file

@ -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",
)

View file

@ -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:

View file

@ -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