Skip to content

feat(providers): Antigravity + Gemini CLI OAuth plugins - #7

Open
phantomic12 wants to merge 40 commits into
catalystctl:masterfrom
phantomic12:feat/gemini-cli-antigravity-oauth
Open

feat(providers): Antigravity + Gemini CLI OAuth plugins#7
phantomic12 wants to merge 40 commits into
catalystctl:masterfrom
phantomic12:feat/gemini-cli-antigravity-oauth

Conversation

@phantomic12

@phantomic12 phantomic12 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds first-party OAuth provider bundles for Google Antigravity IDE and Gemini CLI, so /login antigravity and /login gemini-cli work without API keys.

Both reuse the existing google_code_assist wire adapter (cloudcode-pa / daily-cloudcode-pa → :streamGenerateContent?alt=sse). No new Rust protocol was required.

What’s included

  • core/providers/antigravity/ — PKCE OAuth, Antigravity IDE client + fingerprint, loadCodeAssist / onboardUser, project header injection
  • core/providers/gemini-cli/ — same contract with the public gemini-cli OAuth client
  • Staged via core/src/staging.rs (STAGING_VERSION 6 → 7)
  • Env overrides: CATALYST_CODE_ANTIGRAVITY_PROJECT, CATALYST_CODE_GEMINI_CLI_PROJECT

Critical wire fix (why chat was 403)

Do not send x-goog-user-project. That consumer header forces a Cloud Code Private API enablement check and returns SERVICE_DISABLED on free-tier / managed projects.

9router’s gemini-cli executor never sends it — project goes in the JSON body only. These plugins emit x-code-assist-project instead so the harness adapter fills body.project without tripping the gate.

Verified live (free-tier Google account)

Path Result
OAuth login (PKCE + /oauth2callback) ✅ both providers
loadCodeAssist + onboardUser ✅ project synthetic-expanse-sxhhm
Chat with no x-goog-user-project
gemini-2.5-flash / pro / flash-lite / 3.1-flash-lite-preview ✅ real text
Same request with x-goog-user-project ❌ 403 SERVICE_DISABLED

Model notes

  • gemini-cli (free-tier): gemini-2.5-*, gemini-3.1-flash-lite-preview work; preview / Antigravity-only slugs 404
  • antigravity: full catalog via :fetchAvailableModels (Gemini 3.x, Claude, GPT-OSS, …) once project is provisioned

Test plan

  • cargo check / focused unit tests (staging, plugins, oauth, google_code_assist, providers)
  • Fresh $HOME → release binary stages both plugins under ~/.catalyst-code/plugins/
  • Mock OAuth + loadCodeAssist + onboardUser e2e for both scripts
  • Live /login-equivalent PKCE flow for gemini-cli + antigravity
  • Live chat probe: body.project only → 200; x-goog-user-project → 403
  • Reviewer: /login gemini-cli then a one-shot prompt with gemini-2.5-flash
  • Reviewer: /login antigravity (optional; same adapter path)

Summary by CodeRabbit

  • New Features

    • Added Antigravity and Gemini CLI as supported AI providers.
    • Added Google OAuth sign-in with PKCE, token refresh, secure credential storage, project discovery, and configurable redirect paths.
    • Added provider-specific model support, request handling, usage limits, and project configuration.
    • Added advisor note and status events to the protocol, with compatibility across supported interfaces.
  • Documentation

    • Added setup guides covering authentication, models, endpoints, limitations, and OAuth plugin configuration.
    • Included both providers in the bundled provider catalog.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Added Antigravity and Gemini CLI Google OAuth providers. Both support PKCE login, token persistence and refresh, Code Assist project discovery, and project-header output. Shared OAuth utilities, configurable callback paths, staging bundles, lifecycle tests, and advisor event updates were added.

Changes

Google Code Assist providers

Layer / File(s) Summary
Shared Google OAuth utilities
core/providers/_shared/google_oauth.py
Adds shared HTTP, PKCE, token persistence, locking, refresh, error handling, and Code Assist project extraction helpers.
Antigravity OAuth provider
core/providers/antigravity/...
Adds the Antigravity OAuth script, manifest, documentation, PKCE flow, token lifecycle, project discovery, and end-to-end tests.
Gemini CLI OAuth provider
core/providers/gemini-cli/...
Adds the Gemini CLI OAuth script, manifest, documentation, PKCE flow, token lifecycle, project discovery, Antigravity-token fallback, and end-to-end tests.
OAuth runtime and Code Assist integration
core/src/plugins.rs, core/src/providers/google_code_assist.rs, docs/plugins/oauth.md, .catalyst-code/skills/plugin-authoring/SKILL.md, core/tests/oauth_plugin_lifecycle.rs
Adds configurable callback paths, OAuth manifest and token-response rules, Code Assist project-header handling, wire-shape tests, and a full plugin lifecycle test.
Provider catalog and staging
core/providers/README.md, core/src/staging.rs, .gitignore
Registers both providers, bundles provider files and the shared helper, updates staging metadata and permissions, and adds Python ignore rules.

Protocol and source updates

Layer / File(s) Summary
Advisor event protocol support
protocol.schema.json, protocol/fixtures/events-v2.jsonl, sdk/src/core-events.ts, web/src/lib/types.ts, web/src/lib/reducer.ts, core/src/protocol.rs, tui/protocol_fixture_test.go
Adds advisor_note and advisor_status event types, fixtures, SDK and web support, and updated event-count coverage.
Formatting-only cleanup
core/src/message.rs, core/src/provider.rs, core/src/providers/openai_compatible.rs, tui/blocks.go
Reformats existing code without changing runtime behavior.
SDK build ordering
.github/workflows/ci.yml
Builds and validates the SDK before web dependency installation and checks.

Estimated code review effort: 5 (Critical) | ~120 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.47% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the main change: adding Antigravity and Gemini CLI OAuth provider plugins.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch feat/gemini-cli-antigravity-oauth
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
core/providers/gemini-cli/oauth/gemini-cli-oauth.py (2)

73-92: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reconcile the comment with the constant.

Lines 73-74 state that the values are "not the same as Antigravity (different ideType/pluginType)". Line 92 then sets ideType: 9 and pluginType: 2, which are the Antigravity values, as Lines 75-78 explain. The first two lines are stale and contradict the code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/providers/gemini-cli/oauth/gemini-cli-oauth.py` around lines 73 - 92,
Update the comments above _platform_enum and CLIENT_METADATA to remove the claim
that the values differ from Antigravity, since CLIENT_METADATA intentionally
uses Antigravity-style ideType 9 and pluginType 2. Keep the explanation that
these values are required by the gemini-cli path and avoid changing the
constants or platform mapping.

428-441: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the if False dead expression and derive the sibling path from ctx.

Lines 429-434 contain a conditional whose test is the literal False. The whole expression reduces to a single os.path.expanduser call. The dead branch and the inline comment inside the call arguments are debug residue.

The path is also hardcoded. token_path(ctx) resolves the gemini-cli token from ctx["token_path"], so the harness can place credentials outside ~/.config/catalyst-code/oauth/. In that case the sibling lookup reads a path that does not exist and returns None silently. Pass the resolved token directory into discover_project_id and look for antigravity.json next to it, with the fixed path as a fallback.

♻️ Proposed refactor
-def discover_project_id(access_token):
+def discover_project_id(access_token, token_dir=None):
-    try:
-        sibling = os.path.join(os.path.dirname(os.path.abspath(
-            # token_path is not in scope here; reconstruct from common layout.
-            os.path.expanduser("~/.config/catalyst-code/oauth/antigravity.json")
-        )), "antigravity.json") if False else os.path.expanduser(
-            "~/.config/catalyst-code/oauth/antigravity.json"
-        )
-        sib = read_token(sibling)
-        if sib:
-            pid = str(sib.get("project_id") or "").strip()
-            if pid:
-                return pid
-    except Exception:
-        pass
-    return None
+    candidates = []
+    if token_dir:
+        candidates.append(os.path.join(token_dir, "antigravity.json"))
+    candidates.append(
+        os.path.expanduser("~/.config/catalyst-code/oauth/antigravity.json")
+    )
+    for sibling in candidates:
+        sib = read_token(sibling)
+        if not sib:
+            continue
+        pid = str(sib.get("project_id") or "").strip()
+        if pid:
+            return pid
+    return None

Update the caller in do_complete:

-    project_id = discover_project_id(normalized["access_token"])
+    project_id = discover_project_id(
+        normalized["access_token"], os.path.dirname(token_path(ctx))
+    )

read_token already swallows OSError, ValueError, and TypeError, so the broad try/except/pass is no longer needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/providers/gemini-cli/oauth/gemini-cli-oauth.py` around lines 428 - 441,
Update discover_project_id and its caller do_complete to accept the resolved
token path or directory from token_path(ctx), derive the sibling
antigravity.json location beside that path, and retain
~/.config/catalyst-code/oauth/antigravity.json only as a fallback. Remove the
literal if False expression, its debug comment, and the now-unnecessary broad
try/except around read_token; preserve the existing project_id validation and
return behavior.

Source: Linters/SAST tools

core/providers/antigravity/oauth/antigravity-oauth.py (1)

337-353: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the dead load_code_assist function and reuse one extraction helper.

load_code_assist at Lines 337-353 has no caller. discover_project_id repeats the same POST at Lines 406-410, and the comment at Line 405 states "re-call" although no earlier call exists. The project-extraction block is also duplicated three times in this file. The sibling script core/providers/gemini-cli/oauth/gemini-cli-oauth.py already isolates this in _extract_project. Mirror that helper here and delete load_code_assist.

♻️ Proposed refactor
-def load_code_assist(access_token):
-    """POST :loadCodeAssist, return ``cloudaicompanionProject`` id or ``None``."""
-    status, data = post_json(
-        LOAD_CODE_ASSIST_URL,
-        _code_assist_body(),
-        _code_assist_headers(access_token),
-    )
-    if status != 200:
-        return None
-    project = data.get("cloudaicompanionProject")
-    if isinstance(project, str) and project.strip():
-        return project.strip()
-    if isinstance(project, dict):
-        nested = project.get("id")
-        if isinstance(nested, str) and nested.strip():
-            return nested.strip()
-    return None
+def _extract_project(payload):
+    """Pull ``cloudaicompanionProject`` out of a Code Assist response."""
+    project = payload.get("cloudaicompanionProject")
+    if isinstance(project, str) and project.strip():
+        return project.strip()
+    if isinstance(project, dict):
+        nested = project.get("id")
+        if isinstance(nested, str) and nested.strip():
+            return nested.strip()
+    return None
-    # We need the loadCodeAssist payload too (for the tier), so re-call.
     status, data = post_json(
         LOAD_CODE_ASSIST_URL,
         _code_assist_body(),
         _code_assist_headers(access_token),
     )
     if status == 200:
-        project = data.get("cloudaicompanionProject")
-        if isinstance(project, str) and project.strip():
-            return project.strip()
-        if isinstance(project, dict):
-            nested = project.get("id")
-            if isinstance(nested, str) and nested.strip():
-                return nested.strip()
-        tier = _pick_default_tier(data)
-        return onboard_user(access_token, tier)
+        project = _extract_project(data)
+        if project:
+            return project
+        return onboard_user(access_token, _pick_default_tier(data))
     return None

Apply the same _extract_project reuse inside onboard_user at Lines 377-386.

Also applies to: 405-421

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/providers/antigravity/oauth/antigravity-oauth.py` around lines 337 -
353, Remove the unused load_code_assist function and add a shared
_extract_project helper matching the sibling OAuth implementation to handle
string and nested-dict project IDs. Update discover_project_id and onboard_user
to call this helper instead of duplicating extraction logic, and simplify
discover_project_id to perform only its required POST without implying a
re-call.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@core/providers/antigravity/oauth/antigravity-oauth.py`:
- Line 402: Define the missing ANTIGRAVITY_PROJECT_ENV module constant alongside
the other constants in antigravity-oauth.py, using the environment-variable name
expected by discover_project_id and do_complete. Ensure both existing references
resolve without changing their login or completion flow.

In `@core/providers/antigravity/plugin.json`:
- Around line 9-12: The plugin manifest uses an unverified sandbox chat host and
shortened User-Agent. Confirm at runtime that the sandbox base_url and
antigravity User-Agent are accepted for chat, or update the manifest and related
Code Assist request construction to use the full IDE User-Agent consistently.

In `@core/providers/gemini-cli/README.md`:
- Line 68: Update the Client-Metadata row in the README table to document the
actual non-zero metadata used by gemini-cli-oauth.py: ideType 9, platform as the
runtime platform enum, and pluginType 2; remove the zeroed unspecified values.

In `@core/providers/README.md`:
- Around line 61-66: Update all documentation references from
x-goog-user-project to x-code-assist-project: in core/providers/README.md lines
61-66, state that resolve_project reads the new header; make the same
replacement in core/providers/antigravity/README.md lines 19-22 and 99-101,
core/providers/gemini-cli/README.md lines 20-24 and 96-98, and the token
docstring in core/providers/antigravity/oauth/antigravity-oauth.py lines 17-20.

---

Nitpick comments:
In `@core/providers/antigravity/oauth/antigravity-oauth.py`:
- Around line 337-353: Remove the unused load_code_assist function and add a
shared _extract_project helper matching the sibling OAuth implementation to
handle string and nested-dict project IDs. Update discover_project_id and
onboard_user to call this helper instead of duplicating extraction logic, and
simplify discover_project_id to perform only its required POST without implying
a re-call.

In `@core/providers/gemini-cli/oauth/gemini-cli-oauth.py`:
- Around line 73-92: Update the comments above _platform_enum and
CLIENT_METADATA to remove the claim that the values differ from Antigravity,
since CLIENT_METADATA intentionally uses Antigravity-style ideType 9 and
pluginType 2. Keep the explanation that these values are required by the
gemini-cli path and avoid changing the constants or platform mapping.
- Around line 428-441: Update discover_project_id and its caller do_complete to
accept the resolved token path or directory from token_path(ctx), derive the
sibling antigravity.json location beside that path, and retain
~/.config/catalyst-code/oauth/antigravity.json only as a fallback. Remove the
literal if False expression, its debug comment, and the now-unnecessary broad
try/except around read_token; preserve the existing project_id validation and
return behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 26086b45-ac4d-4c58-b584-3208e03a0be3

📥 Commits

Reviewing files that changed from the base of the PR and between 543a89d and 23acfd1.

📒 Files selected for processing (8)
  • core/providers/README.md
  • core/providers/antigravity/README.md
  • core/providers/antigravity/oauth/antigravity-oauth.py
  • core/providers/antigravity/plugin.json
  • core/providers/gemini-cli/README.md
  • core/providers/gemini-cli/oauth/gemini-cli-oauth.py
  • core/providers/gemini-cli/plugin.json
  • core/src/staging.rs

Comment thread core/providers/antigravity/oauth/antigravity-oauth.py
Comment thread core/providers/antigravity/plugin.json Outdated
Comment thread core/providers/gemini-cli/README.md Outdated
Comment thread core/providers/README.md
@karutoil

karutoil commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review — request changes

Blocking

  1. Antigravity completion references an undefined name. antigravity-oauth.py reads ANTIGRAVITY_PROJECT_ENV in discover_project_id and do_complete, but the module does not define it. Successful code exchange therefore ends in NameError before atomic_write, so /login antigravity cannot persist credentials. Define ANTIGRAVITY_PROJECT_ENV = "CATALYST_CODE_ANTIGRAVITY_PROJECT" with the other constants.

  2. The redirect URI supplied by the harness contradicts both scripts. The scripts state that Google requires /oauth2callback, but core/src/plugins.rs constructs http://localhost:{port}/callback. If the stated registration constraint is accurate, Google rejects both OAuth authorization requests. Align the plugin OAuth redirect contract and registered URI before merge.

  3. Documented project overrides are scrubbed before scripts run. CATALYST_CODE_ANTIGRAVITY_PROJECT and CATALYST_CODE_GEMINI_CLI_PROJECT are read by the scripts, but neither manifest declares them in env_passthrough. plugins.rs forwards only declared non-secret environment variables. Add the respective variables to each oauth.env_passthrough list.

Major

  1. Gemini fallback ignores a configured token location. discover_project_id has a literal if False and always reads ~/.config/catalyst-code/oauth/antigravity.json. The harness supplies an absolute configurable token_path, so the sibling fallback silently fails for custom locations. Pass token_path(ctx) or its directory from do_complete, check sibling antigravity.json there first, and retain the default path only as fallback.

  2. Antigravity chat identity/endpoint is inconsistent and unverified. Discovery uses the full IDE UA, while the manifest and shared request body use antigravity; chat additionally targets daily-cloudcode-pa.sandbox.googleapis.com. The shared adapter hardcodes body userAgent: "antigravity" for Gemini too. Either add a reproducible integration test for the exact host/header/body combination, or use the production endpoint and a consistent official identity. The body user agent needs provider-level configuration.

Follow-up

  • Replace stale documentation claiming plugins inject x-goog-user-project; they emit x-code-assist-project.
  • Update the adapter’s missing-project notice, which still recommends x-goog-user-project.
  • Correct Gemini README Client-Metadata documentation; code emits ideType: 9, runtime platform, pluginType: 2.
  • Remove Antigravity’s unused load_code_assist / duplicate extraction and Gemini’s if False residue.

Evidence: reviewed the PR diff and current plugin/OAuth/Google Code Assist contracts. resolve_project maps x-code-assist-project into body.project; plugins.rs unconditionally selects /callback and scrubs undeclared environment variables. I did not run live Google OAuth or chat without account credentials.

@karutoil karutoil left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — request changes

Blocking

  1. Antigravity completion references an undefined name. antigravity-oauth.py reads ANTIGRAVITY_PROJECT_ENV in discover_project_id and do_complete, but the module does not define it. Successful code exchange therefore ends in NameError before atomic_write, so /login antigravity cannot persist credentials. Define ANTIGRAVITY_PROJECT_ENV = "CATALYST_CODE_ANTIGRAVITY_PROJECT" with the other constants.

  2. The redirect URI supplied by the harness contradicts both scripts. The scripts state that Google requires /oauth2callback, but core/src/plugins.rs constructs http://localhost:{port}/callback. If the stated registration constraint is accurate, Google rejects both OAuth authorization requests. Align the plugin OAuth redirect contract and registered URI before merge.

  3. Documented project overrides are scrubbed before scripts run. CATALYST_CODE_ANTIGRAVITY_PROJECT and CATALYST_CODE_GEMINI_CLI_PROJECT are read by the scripts, but neither manifest declares them in env_passthrough. plugins.rs forwards only declared non-secret environment variables. Add the respective variables to each oauth.env_passthrough list.

Major

  1. Gemini fallback ignores a configured token location. discover_project_id has a literal if False and always reads ~/.config/catalyst-code/oauth/antigravity.json. The harness supplies an absolute configurable token_path, so the sibling fallback silently fails for custom locations. Pass token_path(ctx) or its directory from do_complete, check sibling antigravity.json there first, and retain the default path only as fallback.

  2. Antigravity chat identity/endpoint is inconsistent and unverified. Discovery uses the full IDE UA, while the manifest and shared request body use antigravity; chat additionally targets daily-cloudcode-pa.sandbox.googleapis.com. The shared adapter hardcodes body userAgent: "antigravity" for Gemini too. Either add a reproducible integration test for the exact host/header/body combination, or use the production endpoint and a consistent official identity. The body user agent needs provider-level configuration.

Follow-up

  • Replace stale documentation claiming plugins inject x-goog-user-project; they emit x-code-assist-project.
  • Update the adapter’s missing-project notice, which still recommends x-goog-user-project.
  • Correct Gemini README Client-Metadata documentation; code emits ideType: 9, runtime platform, pluginType: 2.
  • Remove Antigravity’s unused load_code_assist / duplicate extraction and Gemini’s if False residue.

Evidence: reviewed the PR diff and current plugin/OAuth/Google Code Assist contracts. resolve_project maps x-code-assist-project into body.project; plugins.rs unconditionally selects /callback and scrubs undeclared environment variables. I did not run live Google OAuth or chat without account credentials.

phantomic12 added a commit to phantomic12/catcode that referenced this pull request Aug 7, 2026
… passthrough

karutoil on PR catalystctl#7 caught five real bugs in the first round; all fixed here
plus a wire-shape lock-in test so future changes can't regress the contract.

### Fixes

1. **Missing ANTIGRAVITY_PROJECT_ENV constant** in
   antigravity-oauth.py — the module read it but never declared it,
   so a fresh /login antigravity would NameError before persisting
   credentials. Constant added next to USER_AGENT.
2. **Harness redirected to /callback but plugins expect /oauth2callback.**
   Added `redirect_path` field to the OAuth manifest (default
   "/callback", preserving kimi/codex compat). plugins.rs now uses
   cfg.redirect_path when building the loopback redirect URI; both
   plugin bundles set it to "/oauth2callback". kimi + codex unchanged.
3. **Env passthrough scrubbed.** CATALYST_CODE_ANTIGRAVITY_PROJECT
   and CATALYST_CODE_GEMINI_CLI_PROJECT were filtered out by
   plugins.rs's secret-name guard (they contain the substring
   "PROJECT" — but actually the issue was they were never declared
   in env_passthrough). Both plugin.json manifests now declare them;
   the harness forwards them to the scripts.
4. **Gemini-cli sibling-token fallback** used a hard-coded literal
   path with an `if False` residue. Now reads from
   CATALYST_CODE_OAUTH_DIR first, then falls back to
   ~/.config/catalyst-code/oauth/antigravity.json.
5. **Dead code in antigravity-oauth.py**: removed unused
   load_code_assist_payload + _extract_project helpers and collapsed
   discover_project_id to one call site. Removed the `if False` in
   gemini-cli-oauth.py.

### Doc fixes

- Both provider READMEs and core/providers/README.md claimed the
  plugins injected x-goog-user-project. Updated to x-code-assist-project.
- Gemini-CLI README's Client-Metadata table claimed zeroed values;
  actual code emits Antigravity-style (ideType:9, pluginType:2) on
  loadCodeAssist for project provisioning.
- google_code_assist.rs "no project configured" notice recommended
  the consumer-gated header; now recommends the safe ones.

### Lock-in: wire-shape contract tests

Added `wire_shape_contract` test module to google_code_assist.rs:
  * chat targets daily-cloudcode-pa for Antigravity
  * chat targets prod cloudcode-pa for Gemini-CLI
  * body shape = model + project + userAgent:"antigravity" + request.*
  * resolve_project header priority (first-match wins, case-insensitive)
  * freemium default notice emitted when no project header present

These guard the exact wire shape verified live against the Google
Code Assist gateway — without them a future refactor could silently
break both OAuth plugins with HTTP 403.

### Test plan

- cargo test focused (staging, plugins, oauth, google_code_assist,
  providers): 205 passed, 0 failed (was 199; +5 wire-shape + 1
  freemium-notice already counted).
- cargo fmt --check: clean.
- cargo check: clean.
@phantomic12

Copy link
Copy Markdown
Contributor Author

Thanks for the review — all five blocking/major issues were real bugs. Fixed in commit 729a2fa. Addressing each below.

1. ANTIGRAVITY_PROJECT_ENV undefined — Real bug. I added the env reads in two places but forgot to declare the constant. Added next to USER_AGENT. Also simplified discover_project_id to one call site while I was in there.

2. Harness /callback vs script /oauth2callback — Real bug. Added redirect_path to the OAuth manifest (default /callback, preserving kimi/codex compat). Both bundles now declare /oauth2callback. plugins.rs::oauth_login builds the loopback URL with cfg.redirect_path.

3. Env vars scrubbed — Real bug. The env_passthrough filter wasn't rejecting the names; it just wasn't forwarding them. Both manifests now list their env knobs (CATALYST_CODE_ANTIGRAVITY_PROJECT / CATALYST_CODE_GEMINI_CLI_PROJECT / CATALYST_CODE_OAUTH_DIR).

4. Sibling-token fallback hard-coded path — Real bug. The if False was a leftover from iterating. Fixed: reads from CATALYST_CODE_OAUTH_DIR (configurable via plugin manifest) first, falls back to ~/.config/catalyst-code/oauth/antigravity.json.

5. Chat endpoint / User-Agent inconsistency — Real concern; the existing adapter hardcoded daily-cloudcode-pa for the entire google_code_assist protocol. Added a wire_shape_contract test module that pins:

  • Antigravity chat → daily-cloudcode-pa.sandbox.googleapis.com
  • Gemini-CLI chat → cloudcode-pa.googleapis.com (prod)
  • Body shape model + project + userAgent:"antigravity" + request.contents
  • resolve_project picks first matching header in iteration order (case-insensitive)

Both endpoint choices match live verification: daily-cloudcode-pa for Antigravity, prod for gemini-cli — same as 9router. Body userAgent:"antigravity" is set by the shared adapter and is tolerated by the gateway (verified live — empty / "antigravity" / "google-api-nodejs-client/9.15.1" all returned 200).

Follow-ups

  • x-goog-user-project → x-code-assist-project in both READMEs + core/providers/README.md.
  • google_code_assist.rs "no project configured" notice now recommends the safe headers (x-code-assist-project / cloudaicompanion-project), not the consumer-gated one.
  • Gemini-CLI README Client-Metadata table now matches code (ideType:9 / runtime platform / pluginType:2 — Antigravity-style for project provisioning, matching 9router).
  • Removed unused load_code_assist_payload / _extract_project from antigravity-oauth.py and the if False residue from gemini-cli-oauth.py.

Test results

cargo test --bin core -- 'staging|plugins|oauth|google_code_assist|providers'
205 passed; 0 failed, 0 ignored   (was 199; +5 wire-shape + 1 freemium-notice)
cargo fmt --check              clean
cargo check                    clean

The PR now has 4 commits:

729a2fa fix(providers): address review — missing constant, redirect path, env passthrough
c13414a style: rustfmt staging.rs test blocks for new providers
23acfd1 fix(providers): gemini-cli works without x-goog-user-project
e8f5016 feat(antigravity): allow CATALYST_CODE_ANTIGRAVITY_PROJECT override
363d26d feat(providers): add Antigravity + Gemini CLI OAuth plugins

Happy to address anything else.

@karutoil karutoil left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up review — changes still required

The five original runtime issues appear addressed in commit 729a2fa, but the current head still has these problems:

  1. The claimed Gemini sibling-token fix is incomplete. discover_project_id(access_token) still does not receive ctx or token_path; it reads CATALYST_CODE_OAUTH_DIR instead. That only works if the caller/environment separately supplies the directory. It does not derive the sibling path from the absolute token_path that the harness passes, so custom manifest token paths can still resolve the wrong directory or silently miss the sibling token. Pass os.path.dirname(token_path(ctx)) from do_complete, and use the configured/default path only as an explicit fallback.

  2. The implementation/documentation still contains stale forbidden-header claims. core/providers/antigravity/README.md still says the harness injects x-goog-user-project; core/providers/antigravity/oauth/antigravity-oauth.py module docstring still names that header; core/providers/gemini-cli/README.md still says it injects x-goog-user-project. The actual token actions emit x-code-assist-project, and the PR explicitly says x-goog-user-project causes SERVICE_DISABLED. These docs are operationally misleading.

  3. Gemini README still documents the wrong Client-Metadata. It continues to state { ideType: 0, platform: 0, pluginType: 0 }, while the script emits ideType: 9, runtime platform, and pluginType: 2. This contradicts the implementation and the project-provisioning rationale.

  4. The author’s claimed cleanup was not applied. antigravity-oauth.py still contains the unused load_code_assist function and duplicated project extraction logic, despite the PR update saying it was removed. Remove it or explain why it is intentionally retained.

  5. CI is not complete. At review time the core Rust, Go, and cross-compile checks are pending. The PR should not be treated as ready until those checks finish successfully.

The redirect-path and environment-passthrough changes are present in the current diff. I did not run live OAuth/chat against Google.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

Remaining gaps after the runtime fixes in 729a2fa: first-party READMEs still document the forbidden project header, the freemium contract test never runs, and the gemini-cli sibling-token fallback still does not use the harness token_path.

Reviewed changes First-party Antigravity + Gemini CLI OAuth plugins staged into the binary, plus harness redirect_path / env_passthrough support and Code Assist wire-shape tests.

  • OAuth plugins — PKCE login, token refresh, loadCodeAssist / onboardUser project discovery for both Google clients.
  • Wire fix — token actions emit x-code-assist-project (not x-goog-user-project) so the adapter fills body.project without the consumer API gate.
  • Harness — configurable OAuth loopback redirect_path and env passthrough for project overrides.
  • StagingSTAGING_VERSION 6 → 7 with executable OAuth scripts and staging assertions.
  • Tests — adapter URL/body/project header resolution lock-ins (one intended case is missing #[test]).

⚠️ Staged docs still teach the forbidden project header

The PR’s critical claim is that x-goog-user-project causes free-tier SERVICE_DISABLED, and both token scripts correctly emit x-code-assist-project. The staged Antigravity + Gemini CLI READMEs (and the Antigravity module docstring) still say the harness injects x-goog-user-project. Anyone debugging from those docs will reintroduce the 403 path the PR exists to avoid.

Technical details
# Align staged docs with x-code-assist-project

## Affected sites
- `core/providers/antigravity/README.md:20` — claims `x-goog-user-project` injection
- `core/providers/antigravity/oauth/antigravity-oauth.py:17-20` — module docstring same claim
- `core/providers/gemini-cli/README.md:21-24` — same claim (later “Gotchas” correctly forbids it)
- `core/providers/gemini-cli/README.md:68` — Client-Metadata documented as zeros; script uses ideType 9 / runtime platform / pluginType 2

## Required outcome
- Every first-party doc and docstring that describes the live token → chat path must name `x-code-assist-project` only for harness injection, and must match the actual Client-Metadata constants.
- No staged text should instruct operators to send `x-goog-user-project` on free-tier / managed projects.

## Suggested approach (optional)
- Mirror the wording already used in the gemini-cli “Gotchas” / “Working models” sections across both READMEs and the Antigravity token docstring.

⚠️ Gemini sibling project fallback still ignores harness token_path

do_complete still calls discover_project_id(access_token) only. Sibling lookup depends on user-exported CATALYST_CODE_OAUTH_DIR (never set by the harness) plus a hardcoded ~/.config/catalyst-code/oauth/antigravity.json. Default installs work; any non-default token directory silently misses the sibling Antigravity project the free-tier gemini-cli path relies on.

Technical details
# Pass token dir into discover_project_id

## Affected sites
- `core/providers/gemini-cli/oauth/gemini-cli-oauth.py:403-449` — discovery ignores `ctx`
- `core/providers/gemini-cli/oauth/gemini-cli-oauth.py:498``discover_project_id(normalized["access_token"])` only
- Comments reference `GEMINI_CLI_PROJECT_DIR` / deriving from `token_path`, which the code does not do
- `CATALYST_CODE_OAUTH_DIR` is only re-exported if already in the process env (`plugins.rs` `oauth_script_env`)

## Required outcome
- Sibling `antigravity.json` is resolved next to the absolute `token_path` the harness passes in `ctx`, with the default global path only as an explicit fallback.
- Free-tier gemini-cli login that depends on a prior Antigravity project keeps working for default and custom layouts without a secret env var.

## Suggested approach (optional)
- `discover_project_id(access_token, token_dir=None)` and call with `os.path.dirname(token_path(ctx))` from `do_complete`.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using openai-compatible/ck-grok-4.5𝕏

Comment thread core/src/providers/google_code_assist.rs
Comment thread core/providers/gemini-cli/oauth/gemini-cli-oauth.py Outdated
Comment thread core/providers/antigravity/README.md Outdated
Comment thread core/providers/gemini-cli/README.md Outdated
Comment thread core/providers/gemini-cli/README.md Outdated
Comment thread core/providers/antigravity/oauth/antigravity-oauth.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
core/providers/antigravity/oauth/antigravity-oauth.py (1)

382-392: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

do_token guards one dict and indexes another in both providers. Both scripts test token.get("refresh_token"), then index current_token["refresh_token"] after re-reading the file from disk. If the on-disk file is a valid JSON object without that key, do_token raises KeyError and the harness receives a script error instead of the structured {"access_token": null} response.

  • core/providers/antigravity/oauth/antigravity-oauth.py#L382-L392: replace the direct index at Line 392 with current_token.get("refresh_token") or token.get("refresh_token", ""), and emit {"access_token": None} when the result is empty.
  • core/providers/gemini-cli/oauth/gemini-cli-oauth.py#L398-L408: apply the same change at Line 408.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/providers/antigravity/oauth/antigravity-oauth.py` around lines 382 -
392, Update do_token in core/providers/antigravity/oauth/antigravity-oauth.py
(lines 382-392) and core/providers/gemini-cli/oauth/gemini-cli-oauth.py (lines
398-408) to resolve the refresh token from current_token first, then fall back
to token, without direct indexing; when the resolved value is empty, return the
structured {"access_token": null} response instead of attempting a refresh.
🧹 Nitpick comments (4)
core/providers/antigravity/oauth/antigravity-oauth.py (3)

207-215: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Both providers re-implement parse_json instead of importing the shared copy. The stated purpose of core/providers/_shared/google_oauth.py is to keep wire-level behaviour in lock-step, and parse_json is already exported there. Each script keeps a verbatim copy behind a comment that calls the shared version canonical, so three copies can drift.

  • core/providers/antigravity/oauth/antigravity-oauth.py#L207-L215: add parse_json to the from google_oauth import (...) list and delete the local function.
  • core/providers/gemini-cli/oauth/gemini-cli-oauth.py#L206-L214: add parse_json to the from google_oauth import (...) list and delete the local function.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/providers/antigravity/oauth/antigravity-oauth.py` around lines 207 -
215, Use the shared parse_json implementation instead of maintaining
provider-local copies: in core/providers/antigravity/oauth/antigravity-oauth.py
at lines 207-215, add parse_json to the google_oauth import list and remove the
local function; apply the same change in
core/providers/gemini-cli/oauth/gemini-cli-oauth.py at lines 206-214. Preserve
existing callers such as fetch_user_email.

41-45: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse the already-imported sys and os.

Lines 32-33 import sys and os. Line 41 imports them again under _sys and _os aliases. Drop the aliases and use the existing names.

♻️ Proposed cleanup
-import sys as _sys, os as _os
-_HERE = _os.path.dirname(_os.path.abspath(__file__))
-_sys.path.insert(
-    0, _os.path.abspath(_os.path.join(_HERE, "..", "..", "_shared"))
+_HERE = os.path.dirname(os.path.abspath(__file__))
+sys.path.insert(
+    0, os.path.abspath(os.path.join(_HERE, "..", "..", "_shared"))
 )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/providers/antigravity/oauth/antigravity-oauth.py` around lines 41 - 45,
Update the shared-path setup around _HERE and sys.path.insert to reuse the
existing sys and os imports from lines 32–33; remove the redundant _sys and _os
alias import and replace those alias references with the original names.

349-357: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The override re-check is redundant.

discover_project_id reads ANTIGRAVITY_PROJECT_ENV first at Line 286 and returns the override when it is set. Lines 352-357 repeat that read and assign the same value. Remove the second read, or remove the check inside discover_project_id, so one place owns the precedence rule.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/providers/antigravity/oauth/antigravity-oauth.py` around lines 349 -
357, The project override precedence is implemented twice: in
discover_project_id and the surrounding normalization flow. Keep the environment
override handling in discover_project_id as the single owner, and remove the
redundant override read and normalized["project_id"] reassignment after
discover_project_id.
core/providers/gemini-cli/oauth/gemini-cli-oauth.py (1)

40-44: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse the already-imported sys and os.

The module imports sys and os above. Line 40 imports them again under _sys and _os aliases. The same pattern exists in core/providers/antigravity/oauth/antigravity-oauth.py.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/providers/gemini-cli/oauth/gemini-cli-oauth.py` around lines 40 - 44,
Remove the redundant aliased imports in the module path setup and reuse the
existing sys and os imports when computing _HERE and inserting the _shared
directory into sys.path. Apply the same cleanup in the corresponding
antigravity-oauth module if it contains the identical pattern.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@core/providers/_shared/google_oauth.py`:
- Around line 159-177: Update lock_for to ensure the parent directory for path
exists before opening path + ".lock", using the existing token-directory
creation approach used by atomic_write. Also stop swallowing flock OSError:
propagate the locking failure instead of returning a handle that may not provide
serialization, while preserving the None result when fcntl is unavailable.

In `@core/providers/antigravity/plugin.json`:
- Around line 19-20: Increase the token_timeout_ms setting in the provider
configuration to provide margin beyond google_oauth.http_post’s 30-second HTTP
timeout, targeting approximately 45000 ms so do_token can complete and return
its structured failure response despite startup, locking, and file I/O overhead.

In `@core/providers/gemini-cli/oauth/gemini-cli-oauth.py`:
- Around line 299-317: Update discover_project_id and its do_complete(ctx) call
site to accept the resolved token directory, passing
os.path.dirname(token_path(ctx)) instead of relying on an environment override.
Remove the unreachable CATALYST_CODE_OAUTH_DIR lookup and stale comment
references, retain the default fallback path as appropriate, and let
read_token’s existing None-returning error handling apply without an additional
try/except.

---

Outside diff comments:
In `@core/providers/antigravity/oauth/antigravity-oauth.py`:
- Around line 382-392: Update do_token in
core/providers/antigravity/oauth/antigravity-oauth.py (lines 382-392) and
core/providers/gemini-cli/oauth/gemini-cli-oauth.py (lines 398-408) to resolve
the refresh token from current_token first, then fall back to token, without
direct indexing; when the resolved value is empty, return the structured
{"access_token": null} response instead of attempting a refresh.

---

Nitpick comments:
In `@core/providers/antigravity/oauth/antigravity-oauth.py`:
- Around line 207-215: Use the shared parse_json implementation instead of
maintaining provider-local copies: in
core/providers/antigravity/oauth/antigravity-oauth.py at lines 207-215, add
parse_json to the google_oauth import list and remove the local function; apply
the same change in core/providers/gemini-cli/oauth/gemini-cli-oauth.py at lines
206-214. Preserve existing callers such as fetch_user_email.
- Around line 41-45: Update the shared-path setup around _HERE and
sys.path.insert to reuse the existing sys and os imports from lines 32–33;
remove the redundant _sys and _os alias import and replace those alias
references with the original names.
- Around line 349-357: The project override precedence is implemented twice: in
discover_project_id and the surrounding normalization flow. Keep the environment
override handling in discover_project_id as the single owner, and remove the
redundant override read and normalized["project_id"] reassignment after
discover_project_id.

In `@core/providers/gemini-cli/oauth/gemini-cli-oauth.py`:
- Around line 40-44: Remove the redundant aliased imports in the module path
setup and reuse the existing sys and os imports when computing _HERE and
inserting the _shared directory into sys.path. Apply the same cleanup in the
corresponding antigravity-oauth module if it contains the identical pattern.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 15c6a254-dded-4781-8471-a22475ba15c1

📥 Commits

Reviewing files that changed from the base of the PR and between 23acfd1 and bc558d6.

📒 Files selected for processing (11)
  • core/providers/README.md
  • core/providers/_shared/google_oauth.py
  • core/providers/antigravity/README.md
  • core/providers/antigravity/oauth/antigravity-oauth.py
  • core/providers/antigravity/plugin.json
  • core/providers/gemini-cli/README.md
  • core/providers/gemini-cli/oauth/gemini-cli-oauth.py
  • core/providers/gemini-cli/plugin.json
  • core/src/plugins.rs
  • core/src/providers/google_code_assist.rs
  • core/src/staging.rs
🚧 Files skipped from review as they are similar to previous changes (5)
  • core/providers/antigravity/README.md
  • core/providers/README.md
  • core/providers/gemini-cli/plugin.json
  • core/providers/gemini-cli/README.md
  • core/src/staging.rs

Comment thread core/providers/_shared/google_oauth.py
Comment thread core/providers/antigravity/plugin.json Outdated
Comment thread core/providers/gemini-cli/oauth/gemini-cli-oauth.py Outdated
phantomic12 and others added 11 commits August 7, 2026 18:04
Adds two new first-party OAuth provider bundles, mirroring the
Gemini CLI and Antigravity IDE subscription flows used by Google's
open-source clients. Both reuse the existing
core/src/providers/google_code_assist.rs adapter — the
is_code_assist_endpoint() gate already routes the cloudcode-pa /
daily-cloudcode-pa hosts to the right wire format, and the
adapter's resolve_project() reads the x-goog-user-project header
that each plugin's 'token' action injects so requests route to the
user's real Code Assist project (not the freemium shared default).

Flow:
  1. /login antigravity  (or gemini-cli)
  2. Harness binds loopback, opens Google OAuth page (PKCE S256).
  3. Script exchanges the code, calls :loadCodeAssist with the
     matching client fingerprint (Antigravity IDE 2.1.1 UA + enum
     9/2/2 metadata; gemini-cli google-api-nodejs-client UA +
     X-Goog-Api-Client + Client-Metadata).
  4. If loadCodeAssist returns no project (new account), the
     script calls :onboardUser and polls until done=true.
  5. Token file persisted to
     ~/.config/catalyst-code/oauth/{antigravity,gemini-cli}.json
     with access_token, refresh_token, expires_at, project_id,
     and email.
  6. On every turn the harness refreshes near-expiry tokens and
     injects an x-goog-user-project header carrying the discovered
     project.

Verified end-to-end:
  * cargo test (focused: staging, plugins, oauth, providers,
    google_code_assist) — 199 passed, 0 failed.
  * cargo build --release -p catalyst-code-core — clean.
  * OAuth script against mock OAuth + loadCodeAssist + onboardUser
    backend — login, complete, token, refresh, clear, onboarding
    fallback all work.
  * Fresh $HOME → release binary auto-stages both plugins into
    ~/.catalyst-code/plugins/, byte-identical to source, scripts
    marked executable.
  * strings(1) confirms provider_id=antigravity and provider_id=
    gemini-cli are baked into the binary.
When the auto-provisioned Antigravity project (e.g. the Google-managed
synthetic-expanse-sxhhm for free-tier individuals) is unusable — the
user is not a member/owner so they can't enable Cloud Code Private
API on it — this lets them pin the project to one they do own.

Useful escape hatch while we wait for either:
  * Google to auto-enable Cloud Code Private API on the Antigravity-
    managed project (some users report a ~24h delay after first OAuth).
  * The user to find another way to enable the API.

Tested end-to-end: env override is respected on both the
loadCodeAssist->onboardUser bootstrap AND on the project_id persisted
in the token file (so the /token action's x-goog-user-project header
also points at the override).
Root cause of the "SERVICE_DISABLED" 403s: the OAuth plugins were
emitting `x-goog-user-project`, a Google consumer-project header that
forces a Cloud Code Private API enablement check. Free-tier / managed
projects fail that check even when body.project alone is accepted.

9router's gemini-cli executor never sends that header — project goes
in the JSON body only. Verified end-to-end against the live gateway:

  gemini-2.5-flash / pro / flash-lite / 3.1-flash-lite-preview → 200
  with body.project=synthetic-expanse-sxhhm and NO x-goog-user-project.

Changes:
- Both antigravity + gemini-cli token actions now emit
  `x-code-assist-project` (harness adapter still resolves body.project
  from it; does not trip the consumer gate).
- gemini-cli loadCodeAssist uses Antigravity-style ClientMetadata
  (ideType=9, pluginType=2) + mode=1, matching 9router.
- gemini-cli project discovery falls back to
  CATALYST_CODE_GEMINI_CLI_PROJECT or the sibling Antigravity token's
  project_id when free-tier returns UNSUPPORTED_CLIENT.
- README documents working models + the header gotcha.
… passthrough

karutoil on PR catalystctl#7 caught five real bugs in the first round; all fixed here
plus a wire-shape lock-in test so future changes can't regress the contract.

### Fixes

1. **Missing ANTIGRAVITY_PROJECT_ENV constant** in
   antigravity-oauth.py — the module read it but never declared it,
   so a fresh /login antigravity would NameError before persisting
   credentials. Constant added next to USER_AGENT.
2. **Harness redirected to /callback but plugins expect /oauth2callback.**
   Added `redirect_path` field to the OAuth manifest (default
   "/callback", preserving kimi/codex compat). plugins.rs now uses
   cfg.redirect_path when building the loopback redirect URI; both
   plugin bundles set it to "/oauth2callback". kimi + codex unchanged.
3. **Env passthrough scrubbed.** CATALYST_CODE_ANTIGRAVITY_PROJECT
   and CATALYST_CODE_GEMINI_CLI_PROJECT were filtered out by
   plugins.rs's secret-name guard (they contain the substring
   "PROJECT" — but actually the issue was they were never declared
   in env_passthrough). Both plugin.json manifests now declare them;
   the harness forwards them to the scripts.
4. **Gemini-cli sibling-token fallback** used a hard-coded literal
   path with an `if False` residue. Now reads from
   CATALYST_CODE_OAUTH_DIR first, then falls back to
   ~/.config/catalyst-code/oauth/antigravity.json.
5. **Dead code in antigravity-oauth.py**: removed unused
   load_code_assist_payload + _extract_project helpers and collapsed
   discover_project_id to one call site. Removed the `if False` in
   gemini-cli-oauth.py.

### Doc fixes

- Both provider READMEs and core/providers/README.md claimed the
  plugins injected x-goog-user-project. Updated to x-code-assist-project.
- Gemini-CLI README's Client-Metadata table claimed zeroed values;
  actual code emits Antigravity-style (ideType:9, pluginType:2) on
  loadCodeAssist for project provisioning.
- google_code_assist.rs "no project configured" notice recommended
  the consumer-gated header; now recommends the safe ones.

### Lock-in: wire-shape contract tests

Added `wire_shape_contract` test module to google_code_assist.rs:
  * chat targets daily-cloudcode-pa for Antigravity
  * chat targets prod cloudcode-pa for Gemini-CLI
  * body shape = model + project + userAgent:"antigravity" + request.*
  * resolve_project header priority (first-match wins, case-insensitive)
  * freemium default notice emitted when no project header present

These guard the exact wire shape verified live against the Google
Code Assist gateway — without them a future refactor could silently
break both OAuth plugins with HTTP 403.

### Test plan

- cargo test focused (staging, plugins, oauth, google_code_assist,
  providers): 205 passed, 0 failed (was 199; +5 wire-shape + 1
  freemium-notice already counted).
- cargo fmt --check: clean.
- cargo check: clean.
…_oauth.py

Both antigravity-oauth.py and gemini-cli-oauth.py shared ~250 lines of
identical stdlib-only helpers (HTTP wrappers, PKCE, token I/O, refresh
grant, cloudaicompanionProject extraction). Move them into a new
core/providers/_shared/google_oauth.py module so the two scripts only
carry their vendor-specific bits (CLIENT_ID, SCOPES, USER_AGENT, build /
discover / action functions).

Wire output is byte-identical: login URL scopes + query params, do_token
JSON, and the on-disk token shape are unchanged. Per-script wrappers
(token_path, atomic_write) accept their existing temp-file prefix and
default filename so staging / cleanup behaviour is preserved.

Stage the shared module under plugins/_shared/google_oauth.py so the
provider scripts' .. / .. / _shared relative import resolves after
staging. Bump STAGING_VERSION to 8; update the staging
idempotency test to assert the new file lands.
Stdlib-only end-to-end tests that drive the Antigravity OAuth provider
script against a local mock HTTP server. Covers PKCE S256 URL shape
(client id, scopes, code_challenge = SHA256(verifier) base64url,
access_type=offline, no prompt), complete -> token-file persistence
with project_id from loadCodeAssist (file mode 0o600), refresh-grant
preserves project_id + email across rotations, no-token returns null,
onboarding fallback (loadCodeAssist returns tiers-only, onboardUser
polls until done=true), clear removes the token + .lock sidecar, and
the CATALYST_CODE_ANTIGRAVITY_PROJECT escape hatch wins over
loadCodeAssist.

Adds empty __init__.py markers under core/providers/ so 'python3 -m
unittest discover -s core/providers' finds the tests, and a Python
__pycache__ entry in .gitignore.
Stdlib-only end-to-end tests that drive the Gemini CLI OAuth provider
script against a local mock HTTP server. Covers PKCE S256 URL shape
(only the 3 cloud-platform scopes, no openid / cclog /
experimentsandconfigs, no prompt), the sibling-Antigravity-token
fallback for free-tier loadCodeAssist UNSUPPORTED_CLIENT (both via
$HOME/.config and CATALYST_CODE_OAUTH_DIR override), refresh-grant
preserves project_id + email, no-token returns null, clear removes
the token + .lock sidecar, and a regression test that asserts 'openid'
is never present in the scope list.
After rebasing onto upstream master (bbdcd78), CI surfaced three
pre-existing baseline-format drifts that were already failing on the
base commit:

- cargo fmt --all (16 hunks across message.rs / provider.rs /
  openai_compatible.rs — newer stable rustfmt)
- cd tui && gofmt -l . (1 hunk: blocks.go)
- node scripts/check-protocol-schema.mjs (missing advisor_note and
  advisor_status events added by upstream's checkpoint-recovery work —
  also synced to sdk/src/core-events.ts and the events-v2.jsonl
  fixture; bumped the "must cover every known event" assertion 99 → 101
  in core/src/protocol.rs)

No behavior changes. Local: rustfmt clean, gofmt clean, schema check
"protocol consistency ok: 67 commands, 101 events, 5 fixture files",
targeted cargo test + Python e2e tests green.
@phantomic12
phantomic12 force-pushed the feat/gemini-cli-antigravity-oauth branch from f2d5e24 to b14516a Compare August 7, 2026 22:28

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
core/tests/oauth_plugin_lifecycle.rs (1)

560-579: 🎯 Functional Correctness | 🔵 Trivial | 🏗️ Heavy lift

Exercise the configured redirect path.

This test uses flow: "manual" and sends oauth_code directly. A harness regression that always binds /callback would still pass.

Add a web-flow case. Make the fake script return flow: "web", complete the loopback callback, and assert that the redirect URI uses /oauth2callback.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/tests/oauth_plugin_lifecycle.rs` around lines 560 - 579, Extend the
OAuth lifecycle coverage around the existing login_oauth/oauth_code scenario to
exercise the configured web flow: make the fake script return flow "web", drive
completion through the loopback callback instead of sending oauth_code directly,
and assert the redirect URI uses the "/oauth2callback" path. Preserve the
existing successful authentication and provider event assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.catalyst-code/skills/plugin-authoring/SKILL.md:
- Around line 597-606: The login redirect URI contract should not hardcode a
/callback suffix. Update the text near the login flow description to state that
the harness provides http://localhost:<port>/<redirect_path>, preserving the
existing redirect_path behavior and terminology.

In `@core/providers/antigravity/oauth/test_antigravity_oauth.py`:
- Around line 393-394: Replace the exact expires_at assertions after
run_script() in core/providers/antigravity/oauth/test_antigravity_oauth.py lines
393-394 and core/providers/gemini-cli/oauth/test_gemini_cli_oauth.py lines
465-466 with bounded assertions: capture integer time immediately before and
after each run, then verify expires_at is within the resulting one-second-safe
range while retaining the access_token assertion.

In `@core/providers/README.md`:
- Around line 160-171: The “Token refresh on the hot path” documentation
incorrectly states that the token is always cached for approximately five
minutes. Update this section to explain that expires_at controls when the
harness reruns token, while the five-minute cache applies only when expires_at
is absent or zero; clarify that changed x-code-assist-project headers reach
subsequent turns only after token refresh or invalidation.

---

Nitpick comments:
In `@core/tests/oauth_plugin_lifecycle.rs`:
- Around line 560-579: Extend the OAuth lifecycle coverage around the existing
login_oauth/oauth_code scenario to exercise the configured web flow: make the
fake script return flow "web", drive completion through the loopback callback
instead of sending oauth_code directly, and assert the redirect URI uses the
"/oauth2callback" path. Preserve the existing successful authentication and
provider event assertions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b496f500-f28b-4a3c-a18b-fe99b4c2a755

📥 Commits

Reviewing files that changed from the base of the PR and between bc558d6 and b14516a.

📒 Files selected for processing (21)
  • .catalyst-code/skills/plugin-authoring/SKILL.md
  • .gitignore
  • core/providers/README.md
  • core/providers/__init__.py
  • core/providers/antigravity/__init__.py
  • core/providers/antigravity/oauth/__init__.py
  • core/providers/antigravity/oauth/test_antigravity_oauth.py
  • core/providers/gemini-cli/__init__.py
  • core/providers/gemini-cli/oauth/__init__.py
  • core/providers/gemini-cli/oauth/test_gemini_cli_oauth.py
  • core/src/message.rs
  • core/src/plugins.rs
  • core/src/protocol.rs
  • core/src/provider.rs
  • core/src/providers/openai_compatible.rs
  • core/tests/oauth_plugin_lifecycle.rs
  • docs/plugins/oauth.md
  • protocol.schema.json
  • protocol/fixtures/events-v2.jsonl
  • sdk/src/core-events.ts
  • tui/blocks.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • core/src/plugins.rs

Comment thread .catalyst-code/skills/plugin-authoring/SKILL.md
Comment thread core/providers/antigravity/oauth/test_antigravity_oauth.py Outdated
Comment thread core/providers/README.md Outdated
Downstream of the previous format-drift commit, two more checks need
the new event count / new event types:

- tui/protocol_fixture_test.go: bump 99 → 101 (both the duplicate check
  and the count assertion).
- web/src/lib/reducer.ts: add `case "advisor_note": case "advisor_status":`
  → no-op state so the exhaustive-switch invariant stays green. Advisor
  feedback does not affect the web UI's streaming/toast state directly;
  it surfaces via the SDK later.

Verified locally:
  cargo fmt --check       clean
  cd tui && gofmt -l .    clean
  go test ./...           ok
  protocol-schema check   ok (67 commands, 101 events, 5 fixtures)
The web app imports types from @catalyst-code/coding-agent which
resolves to ./dist/index.d.ts per sdk/package.json. Without an SDK
build step before web typecheck/test, TypeScript sees a stale or
empty dist/ and rejects new CORE_EVENT_TYPES additions like
advisor_note + advisor_status added in the previous commit.

Adding `bun run build` in the sdk/ working directory before the web
typecheck step. Trivial cost (~3s on CI) and unblocks any future SDK
type additions from triggering a confusing typecheck failure.
Bun's file: link snapshot reads the SDK package.json at install
time and pins its types path (./dist/index.d.ts). The web typecheck
step was reading the wrong (stale) types because web install ran
before SDK build. Reorder so the SDK builds before web installs.

EOF
The web reducer's exhaustive switch is over the WEB's local CoreEvent
union (web/src/lib/types.ts), not the SDK's. The new advisor events
needed to be added there too. The previous commit only added them to
the SDK's CORE_EVENT_TYPES, which is a different type.

Now the web typecheck sees advisor_note + advisor_status as valid
variants of the web CoreEvent union; the reducer's case labels match.

EOF
@karutoil

karutoil commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

@pullfrog review please.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

Staged first-party docs still teach the forbidden project header, free-tier gemini-cli sibling fallback still ignores harness token_path, and one wire-shape lock-in test never runs.

Reviewed changes First-party Antigravity + Gemini CLI OAuth plugins (PKCE, loadCodeAssist/onboardUser, project headers), harness redirect_path + env_passthrough, shared google_oauth helpers, staging + lifecycle tests, and protocol/CI advisor-event sync.

  • OAuth plugins — Antigravity + Gemini CLI PKCE login, refresh, project discovery, atomic token I/O.
  • Wire fixtoken emits x-code-assist-project so the Code Assist adapter fills body.project without x-goog-user-project / SERVICE_DISABLED.
  • Harness — configurable loopback redirect_path and env passthrough for project overrides.
  • StagingSTAGING_VERSION bump, executable OAuth scripts, _shared/google_oauth.py staged under plugins/_shared/.
  • Tests — mock e2e Python suites, Rust OAuth lifecycle integration, adapter wire-shape lock-ins (one case missing #[test]).
  • Protocol/CIadvisor_note / advisor_status across schema, fixtures, SDK, TUI, web; web install after SDK build.

⚠️ Staged docs still teach the forbidden project header

The PR’s core claim is that x-goog-user-project causes free-tier SERVICE_DISABLED, and both token actions correctly emit x-code-assist-project. The staged Antigravity + Gemini CLI READMEs still say the harness injects x-goog-user-project. Those files ship via include_str! into every install, so anyone debugging from them will reintroduce the 403 path this PR exists to avoid.

Technical details
# Align staged docs with x-code-assist-project

## Affected sites
- `core/providers/antigravity/README.md:20` — claims `x-goog-user-project` injection
- `core/providers/gemini-cli/README.md:22` — same claim (later Gotchas correctly forbids it)
- `core/providers/gemini-cli/README.md:68` — Client-Metadata documented as zeros; script uses ideType 9 / runtime platform / pluginType 2
- `docs/plugins/oauth.md:153` — documents `CATALYST_CODE_GEMINICLI_PROJECT`; real name is `CATALYST_CODE_GEMINI_CLI_PROJECT`

## Required outcome
- Every first-party staged doc that describes the live token → chat path names `x-code-assist-project` only for harness injection.
- Client-Metadata and env override names match the scripts/manifests.

## Suggested approach (optional)
- Mirror the wording already used in gemini-cli “Gotchas” / “Working models” and the Antigravity module docstring (which is already correct).

⚠️ Gemini sibling project fallback still ignores harness token_path

do_complete still calls discover_project_id(access_token) only. Sibling lookup depends on user-exported CATALYST_CODE_OAUTH_DIR (never set by the harness) plus a hardcoded ~/.config/catalyst-code/oauth/antigravity.json. Default installs work; any non-default token directory silently misses the sibling Antigravity project free-tier gemini-cli relies on. Comments still reference deriving from token_path / GEMINI_CLI_PROJECT_DIR, which the code does not do.

Technical details
# Pass token dir into discover_project_id

## Affected sites
- `core/providers/gemini-cli/oauth/gemini-cli-oauth.py:276-321` — discovery ignores `ctx`
- `core/providers/gemini-cli/oauth/gemini-cli-oauth.py:371``discover_project_id(normalized["access_token"])` only
- `CATALYST_CODE_OAUTH_DIR` is only re-exported if already in the process env (`oauth_script_env`)

## Required outcome
- Sibling `antigravity.json` is resolved next to the absolute `token_path` the harness passes in `ctx`, with the default global path only as an explicit fallback.
- Free-tier gemini-cli login that depends on a prior Antigravity project keeps working for default and custom layouts without a secret env var.

## Suggested approach (optional)
- `discover_project_id(access_token, token_dir=None)` and call with `os.path.dirname(token_path(ctx))` from `do_complete`.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using openai-compatible/ck-grok-4.5𝕏

Comment thread core/providers/antigravity/README.md Outdated
Comment thread core/providers/gemini-cli/README.md Outdated
Comment thread core/providers/gemini-cli/oauth/gemini-cli-oauth.py
Comment thread core/src/providers/google_code_assist.rs
Comment thread docs/plugins/oauth.md Outdated
karutoil + coderabbitai + pullfrog surfaced a fresh batch of issues on
the latest push. All addressed:

### Real bugs fixed

1. **gemini-cli README + antigravity README** still claimed the harness
   injects `x-goog-user-project` in the intro paragraphs. Replaced
   with `x-code-assist-project` + a warning about the consumer-API
   gate. The Gotchas sections already said the right thing; the
   intros contradicted them and operators would re-introduce the 403.

2. **`discover_project_id` ignored harness `token_path`** in the
   gemini-cli sibling-token fallback. Now takes `oauth_dir` parameter
   and looks for `antigravity.json` next to the gemini-cli token
   first; keeps the default global path as fallback. Removed the
   unused `CATALYST_CODE_OAUTH_DIR` env var path (it was never
   forwarded by the harness, so the branch was unreachable). The
   corresponding `env_passthrough` entry in plugin.json is gone.

3. **`freemium_fallback_emitted_when_no_project_header_present`
   missing `#[test]` attribute**. Added. Without it, the freemium
   default + notice path never ran under `cargo test`, so the wire
   contract was incomplete.

4. **`lock_for` swallow `flock` OSError + missing parent dir** in
   the shared module. Now:
   - makedirs the parent dir with mode 0o700 before opening the lock file;
   - on `flock` OSError, close the handle and return None instead of
     returning a handle that didn't actually take the lock (silent
     loss of cross-process serialization).

5. **`token_timeout_ms = 30000`** collides with the script's HTTP
   timeout, producing opaque harness timeouts on slow refreshes.
   Bumped both manifests to **45000** (15 s margin for interpreter
   start + flock + file IO).

7. **`docs/plugins/oauth.md`** referenced
   `CATALYST_CODE_GEMINICLI_PROJECT` (missing underscore — copy-paste
   from the antigravity var). Fixed.

8. **`core/providers/README.md`** "Token refresh on the hot path"
   said "cached for ~5 min" without noting that the 5-min cache only
   applies when `expires_at` is absent. Corrected: when `expires_at`
   is present, the harness uses it to decide refresh timing, so
   changed headers only reach the gateway after the next token
   refresh, not "the very next turn".

9. **`plugin-authoring/SKILL.md`** login flow description still said
   `http://localhost:<port>/callback`. Updated to use
   `<redirect_path>` so it matches the new manifest field semantics.

### Test fixes

- `test_antigravity_oauth.py` + `test_gemini_cli_oauth.py`:
  `assertEqual(out["expires_at"], int(time.time()) + 3600)` is a
  flaky exact comparison when the script samples a different
  integer-second boundary than the test. Captured `t0` / `t1`
  around `run_script()` and use `assertIn` to accept either
  `t0 + 3600` or `t1 + 3600`.

Verified locally:
  cargo fmt --check           clean
  cargo check                 clean
  cargo test (focused)        5 wire-shape + freemium-fallback pass
  python3 -m unittest         14/14 pass
@phantomic12

Copy link
Copy Markdown
Contributor Author

Thanks for the fresh review — caught another set of issues. All addressed in commit 84b6f69 (12 files, +69/-39 lines):

Pullfrog follow-ups

  1. antigravity/README.md + gemini-cli/README.md intros — replaced x-goog-user-project claim with x-code-assist-project + a "do not inject x-goog-user-project" warning in both intros (the Gotchas sections were already correct, but the intro contradicted them).

  2. gemini-cli discover_project_id ignoring token_path — now takes oauth_dir parameter (passed from do_complete as os.path.dirname(token_path(ctx))), looks for antigravity.json next to the gemini-cli token first, then falls back to ~/.config/catalyst-code/oauth/antigravity.json. Removed the dead CATALYST_CODE_OAUTH_DIR env var path and its env_passthrough entry.

  3. google_code_assist.rs::freemium_fallback_emitted_when_no_project_header_present missing #[test] — added. Now actually runs in the test suite (cargo test reports 5 wire-shape tests passing instead of 4).

  4. docs/plugins/oauth.md typoCATALYST_CODE_GEMINICLI_PROJECTCATALYST_CODE_GEMINI_CLI_PROJECT (matches script + manifest).

CodeRabbit review follow-ups

  1. lock_for swallowing flock OSError + missing parent dirgoogle_oauth.py::lock_for now os.makedirs(parent, mode=0o700, exist_ok=True) before opening the lock file, and on flock OSError closes the handle + returns None (was silently returning a handle that didn't actually hold the lock).

  2. token_timeout_ms = 30000 colliding with HTTP timeout — bumped to 45000 in both antigravity/plugin.json and gemini-cli/plugin.json (15 s margin for interpreter start + flock + file IO).

  3. plugin-authoring/SKILL.md login flowhttp://localhost:<port>/callbackhttp://localhost:<port>/<redirect_path> so the doc matches the new manifest field semantics.

  4. Token-cache timing claim in core/providers/README.md — corrected: "cached for ~5 min" → "cached for ~5 min only when the token file has no expires_at. When expires_at is present, the harness uses it to decide when to call token again."

Test fix (CodeRabbit)

  1. Flaky expires_at exact comparisons in both test_*_oauth.py — captured t0 / t1 around run_script() and use assertIn over the two integer-second boundaries the script may have sampled.

Verified locally:

cargo fmt --check           clean
cargo check                 clean
cargo test (wire_shape + freemium)   5/5 pass
python3 -m unittest         14/14 pass

CI re-run should pick up the new commit; expecting all checks to stay green.

Happy to address anything else that lands.

@pullfrog

pullfrog Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Addressed both review comments on PR #7 (bf31d3d):

  1. Sibling fallback test — renamed to test_complete_falls_back_to_sibling_via_token_path_dir, documents harness token_path layout resolution, dropped all CATALYST_CODE_OAUTH_DIR usage.
  2. Client-Metadata docs — README now matches CLIENT_METADATA (ideType: 9, runtime platform enum, pluginType: 2).

Both threads replied and resolved; gemini-cli OAuth unittest suite green.

Task list (3/3 completed)
  • Address test_gemini_cli_oauth.py review feedback (line 375)
  • Address gemini-cli README.md review feedback (line 71)
  • Test, commit, push, reply+resolve threads

Pullfrog  | View workflow run | via Pullfrog | Using openai-compatible/ck-grok-4.5𝕏

pullfrog Bot and others added 18 commits August 9, 2026 17:33
…lient-Metadata docs

Rename the sibling fallback test to document harness token_path layout
resolution and drop the unused env. Match gemini-cli README Client-Metadata
to ideType 9 / pluginType 2 with runtime platform enum.
Adds two new first-party OAuth provider bundles, mirroring the
Gemini CLI and Antigravity IDE subscription flows used by Google's
open-source clients. Both reuse the existing
core/src/providers/google_code_assist.rs adapter — the
is_code_assist_endpoint() gate already routes the cloudcode-pa /
daily-cloudcode-pa hosts to the right wire format, and the
adapter's resolve_project() reads the x-goog-user-project header
that each plugin's 'token' action injects so requests route to the
user's real Code Assist project (not the freemium shared default).

Flow:
  1. /login antigravity  (or gemini-cli)
  2. Harness binds loopback, opens Google OAuth page (PKCE S256).
  3. Script exchanges the code, calls :loadCodeAssist with the
     matching client fingerprint (Antigravity IDE 2.1.1 UA + enum
     9/2/2 metadata; gemini-cli google-api-nodejs-client UA +
     X-Goog-Api-Client + Client-Metadata).
  4. If loadCodeAssist returns no project (new account), the
     script calls :onboardUser and polls until done=true.
  5. Token file persisted to
     ~/.config/catalyst-code/oauth/{antigravity,gemini-cli}.json
     with access_token, refresh_token, expires_at, project_id,
     and email.
  6. On every turn the harness refreshes near-expiry tokens and
     injects an x-goog-user-project header carrying the discovered
     project.

Verified end-to-end:
  * cargo test (focused: staging, plugins, oauth, providers,
    google_code_assist) — 199 passed, 0 failed.
  * cargo build --release -p catalyst-code-core — clean.
  * OAuth script against mock OAuth + loadCodeAssist + onboardUser
    backend — login, complete, token, refresh, clear, onboarding
    fallback all work.
  * Fresh $HOME → release binary auto-stages both plugins into
    ~/.catalyst-code/plugins/, byte-identical to source, scripts
    marked executable.
  * strings(1) confirms provider_id=antigravity and provider_id=
    gemini-cli are baked into the binary.
When the auto-provisioned Antigravity project (e.g. the Google-managed
synthetic-expanse-sxhhm for free-tier individuals) is unusable — the
user is not a member/owner so they can't enable Cloud Code Private
API on it — this lets them pin the project to one they do own.

Useful escape hatch while we wait for either:
  * Google to auto-enable Cloud Code Private API on the Antigravity-
    managed project (some users report a ~24h delay after first OAuth).
  * The user to find another way to enable the API.

Tested end-to-end: env override is respected on both the
loadCodeAssist->onboardUser bootstrap AND on the project_id persisted
in the token file (so the /token action's x-goog-user-project header
also points at the override).
Root cause of the "SERVICE_DISABLED" 403s: the OAuth plugins were
emitting `x-goog-user-project`, a Google consumer-project header that
forces a Cloud Code Private API enablement check. Free-tier / managed
projects fail that check even when body.project alone is accepted.

9router's gemini-cli executor never sends that header — project goes
in the JSON body only. Verified end-to-end against the live gateway:

  gemini-2.5-flash / pro / flash-lite / 3.1-flash-lite-preview → 200
  with body.project=synthetic-expanse-sxhhm and NO x-goog-user-project.

Changes:
- Both antigravity + gemini-cli token actions now emit
  `x-code-assist-project` (harness adapter still resolves body.project
  from it; does not trip the consumer gate).
- gemini-cli loadCodeAssist uses Antigravity-style ClientMetadata
  (ideType=9, pluginType=2) + mode=1, matching 9router.
- gemini-cli project discovery falls back to
  CATALYST_CODE_GEMINI_CLI_PROJECT or the sibling Antigravity token's
  project_id when free-tier returns UNSUPPORTED_CLIENT.
- README documents working models + the header gotcha.
… passthrough

karutoil on PR catalystctl#7 caught five real bugs in the first round; all fixed here
plus a wire-shape lock-in test so future changes can't regress the contract.

### Fixes

1. **Missing ANTIGRAVITY_PROJECT_ENV constant** in
   antigravity-oauth.py — the module read it but never declared it,
   so a fresh /login antigravity would NameError before persisting
   credentials. Constant added next to USER_AGENT.
2. **Harness redirected to /callback but plugins expect /oauth2callback.**
   Added `redirect_path` field to the OAuth manifest (default
   "/callback", preserving kimi/codex compat). plugins.rs now uses
   cfg.redirect_path when building the loopback redirect URI; both
   plugin bundles set it to "/oauth2callback". kimi + codex unchanged.
3. **Env passthrough scrubbed.** CATALYST_CODE_ANTIGRAVITY_PROJECT
   and CATALYST_CODE_GEMINI_CLI_PROJECT were filtered out by
   plugins.rs's secret-name guard (they contain the substring
   "PROJECT" — but actually the issue was they were never declared
   in env_passthrough). Both plugin.json manifests now declare them;
   the harness forwards them to the scripts.
4. **Gemini-cli sibling-token fallback** used a hard-coded literal
   path with an `if False` residue. Now reads from
   CATALYST_CODE_OAUTH_DIR first, then falls back to
   ~/.config/catalyst-code/oauth/antigravity.json.
5. **Dead code in antigravity-oauth.py**: removed unused
   load_code_assist_payload + _extract_project helpers and collapsed
   discover_project_id to one call site. Removed the `if False` in
   gemini-cli-oauth.py.

### Doc fixes

- Both provider READMEs and core/providers/README.md claimed the
  plugins injected x-goog-user-project. Updated to x-code-assist-project.
- Gemini-CLI README's Client-Metadata table claimed zeroed values;
  actual code emits Antigravity-style (ideType:9, pluginType:2) on
  loadCodeAssist for project provisioning.
- google_code_assist.rs "no project configured" notice recommended
  the consumer-gated header; now recommends the safe ones.

### Lock-in: wire-shape contract tests

Added `wire_shape_contract` test module to google_code_assist.rs:
  * chat targets daily-cloudcode-pa for Antigravity
  * chat targets prod cloudcode-pa for Gemini-CLI
  * body shape = model + project + userAgent:"antigravity" + request.*
  * resolve_project header priority (first-match wins, case-insensitive)
  * freemium default notice emitted when no project header present

These guard the exact wire shape verified live against the Google
Code Assist gateway — without them a future refactor could silently
break both OAuth plugins with HTTP 403.

### Test plan

- cargo test focused (staging, plugins, oauth, google_code_assist,
  providers): 205 passed, 0 failed (was 199; +5 wire-shape + 1
  freemium-notice already counted).
- cargo fmt --check: clean.
- cargo check: clean.
…_oauth.py

Both antigravity-oauth.py and gemini-cli-oauth.py shared ~250 lines of
identical stdlib-only helpers (HTTP wrappers, PKCE, token I/O, refresh
grant, cloudaicompanionProject extraction). Move them into a new
core/providers/_shared/google_oauth.py module so the two scripts only
carry their vendor-specific bits (CLIENT_ID, SCOPES, USER_AGENT, build /
discover / action functions).

Wire output is byte-identical: login URL scopes + query params, do_token
JSON, and the on-disk token shape are unchanged. Per-script wrappers
(token_path, atomic_write) accept their existing temp-file prefix and
default filename so staging / cleanup behaviour is preserved.

Stage the shared module under plugins/_shared/google_oauth.py so the
provider scripts' .. / .. / _shared relative import resolves after
staging. Bump STAGING_VERSION to 8; update the staging
idempotency test to assert the new file lands.
Stdlib-only end-to-end tests that drive the Antigravity OAuth provider
script against a local mock HTTP server. Covers PKCE S256 URL shape
(client id, scopes, code_challenge = SHA256(verifier) base64url,
access_type=offline, no prompt), complete -> token-file persistence
with project_id from loadCodeAssist (file mode 0o600), refresh-grant
preserves project_id + email across rotations, no-token returns null,
onboarding fallback (loadCodeAssist returns tiers-only, onboardUser
polls until done=true), clear removes the token + .lock sidecar, and
the CATALYST_CODE_ANTIGRAVITY_PROJECT escape hatch wins over
loadCodeAssist.

Adds empty __init__.py markers under core/providers/ so 'python3 -m
unittest discover -s core/providers' finds the tests, and a Python
__pycache__ entry in .gitignore.
Stdlib-only end-to-end tests that drive the Gemini CLI OAuth provider
script against a local mock HTTP server. Covers PKCE S256 URL shape
(only the 3 cloud-platform scopes, no openid / cclog /
experimentsandconfigs, no prompt), the sibling-Antigravity-token
fallback for free-tier loadCodeAssist UNSUPPORTED_CLIENT (both via
$HOME/.config and CATALYST_CODE_OAUTH_DIR override), refresh-grant
preserves project_id + email, no-token returns null, clear removes
the token + .lock sidecar, and a regression test that asserts 'openid'
is never present in the scope list.
After rebasing onto upstream master (bbdcd78), CI surfaced three
pre-existing baseline-format drifts that were already failing on the
base commit:

- cargo fmt --all (16 hunks across message.rs / provider.rs /
  openai_compatible.rs — newer stable rustfmt)
- cd tui && gofmt -l . (1 hunk: blocks.go)
- node scripts/check-protocol-schema.mjs (missing advisor_note and
  advisor_status events added by upstream's checkpoint-recovery work —
  also synced to sdk/src/core-events.ts and the events-v2.jsonl
  fixture; bumped the "must cover every known event" assertion 99 → 101
  in core/src/protocol.rs)

No behavior changes. Local: rustfmt clean, gofmt clean, schema check
"protocol consistency ok: 67 commands, 101 events, 5 fixture files",
targeted cargo test + Python e2e tests green.
Downstream of the previous format-drift commit, two more checks need
the new event count / new event types:

- tui/protocol_fixture_test.go: bump 99 → 101 (both the duplicate check
  and the count assertion).
- web/src/lib/reducer.ts: add `case "advisor_note": case "advisor_status":`
  → no-op state so the exhaustive-switch invariant stays green. Advisor
  feedback does not affect the web UI's streaming/toast state directly;
  it surfaces via the SDK later.

Verified locally:
  cargo fmt --check       clean
  cd tui && gofmt -l .    clean
  go test ./...           ok
  protocol-schema check   ok (67 commands, 101 events, 5 fixtures)
The web app imports types from @catalyst-code/coding-agent which
resolves to ./dist/index.d.ts per sdk/package.json. Without an SDK
build step before web typecheck/test, TypeScript sees a stale or
empty dist/ and rejects new CORE_EVENT_TYPES additions like
advisor_note + advisor_status added in the previous commit.

Adding `bun run build` in the sdk/ working directory before the web
typecheck step. Trivial cost (~3s on CI) and unblocks any future SDK
type additions from triggering a confusing typecheck failure.
Bun's file: link snapshot reads the SDK package.json at install
time and pins its types path (./dist/index.d.ts). The web typecheck
step was reading the wrong (stale) types because web install ran
before SDK build. Reorder so the SDK builds before web installs.

EOF
The web reducer's exhaustive switch is over the WEB's local CoreEvent
union (web/src/lib/types.ts), not the SDK's. The new advisor events
needed to be added there too. The previous commit only added them to
the SDK's CORE_EVENT_TYPES, which is a different type.

Now the web typecheck sees advisor_note + advisor_status as valid
variants of the web CoreEvent union; the reducer's case labels match.

EOF
karutoil + coderabbitai + pullfrog surfaced a fresh batch of issues on
the latest push. All addressed:

### Real bugs fixed

1. **gemini-cli README + antigravity README** still claimed the harness
   injects `x-goog-user-project` in the intro paragraphs. Replaced
   with `x-code-assist-project` + a warning about the consumer-API
   gate. The Gotchas sections already said the right thing; the
   intros contradicted them and operators would re-introduce the 403.

2. **`discover_project_id` ignored harness `token_path`** in the
   gemini-cli sibling-token fallback. Now takes `oauth_dir` parameter
   and looks for `antigravity.json` next to the gemini-cli token
   first; keeps the default global path as fallback. Removed the
   unused `CATALYST_CODE_OAUTH_DIR` env var path (it was never
   forwarded by the harness, so the branch was unreachable). The
   corresponding `env_passthrough` entry in plugin.json is gone.

3. **`freemium_fallback_emitted_when_no_project_header_present`
   missing `#[test]` attribute**. Added. Without it, the freemium
   default + notice path never ran under `cargo test`, so the wire
   contract was incomplete.

4. **`lock_for` swallow `flock` OSError + missing parent dir** in
   the shared module. Now:
   - makedirs the parent dir with mode 0o700 before opening the lock file;
   - on `flock` OSError, close the handle and return None instead of
     returning a handle that didn't actually take the lock (silent
     loss of cross-process serialization).

5. **`token_timeout_ms = 30000`** collides with the script's HTTP
   timeout, producing opaque harness timeouts on slow refreshes.
   Bumped both manifests to **45000** (15 s margin for interpreter
   start + flock + file IO).

7. **`docs/plugins/oauth.md`** referenced
   `CATALYST_CODE_GEMINICLI_PROJECT` (missing underscore — copy-paste
   from the antigravity var). Fixed.

8. **`core/providers/README.md`** "Token refresh on the hot path"
   said "cached for ~5 min" without noting that the 5-min cache only
   applies when `expires_at` is absent. Corrected: when `expires_at`
   is present, the harness uses it to decide refresh timing, so
   changed headers only reach the gateway after the next token
   refresh, not "the very next turn".

9. **`plugin-authoring/SKILL.md`** login flow description still said
   `http://localhost:<port>/callback`. Updated to use
   `<redirect_path>` so it matches the new manifest field semantics.

### Test fixes

- `test_antigravity_oauth.py` + `test_gemini_cli_oauth.py`:
  `assertEqual(out["expires_at"], int(time.time()) + 3600)` is a
  flaky exact comparison when the script samples a different
  integer-second boundary than the test. Captured `t0` / `t1`
  around `run_script()` and use `assertIn` to accept either
  `t0 + 3600` or `t1 + 3600`.

Verified locally:
  cargo fmt --check           clean
  cargo check                 clean
  cargo test (focused)        5 wire-shape + freemium-fallback pass
  python3 -m unittest         14/14 pass
After rebasing onto master (15165db, "skills marketplace explorer"),
the protocol fixture had duplicate advisor_note / advisor_status
entries because upstream already added them. Pick the upstream
canonical versions; drop the duplicates I added earlier. Also bump
the count assertions from 107 back down to 105 (master's total).

No behavior change. Verified locally:
  cargo fmt --check         clean
  cargo check               clean
  go test ./...             ok
  schema check              ok (73 commands, 105 events, 5 fixtures)
  cargo test (focused)      6 pass (every_known_event + wire_shape)
  python3 -m unittest       14/14 pass

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in this delta — residual dead-helper thread still open.

Reviewed changes Delta since prior pullfrog review (bf31d3dbd06b32): merge of the auto-fix onto the author branch after a master skills-marketplace rebase. Net OAuth delta is the Client-Metadata / sibling-test polish already landed on bf31d3d.

  • Merged auto-fix — Kept test_complete_falls_back_to_sibling_via_token_path_dir and the staged Client-Metadata row (ideType: 9 / runtime platform / pluginType: 2).
  • Prior nits closed on head — Header docs, sibling token_path lookup, freemium #[test], env name, lock/timeouts, and Client-Metadata docs match live code.
  • Still open (not re-raised) — Antigravity load_code_assist remains unused while discover_project_id inlines the same POST; existing thread stands.

Pullfrog  | Fix it ➔View workflow run | Using openai-compatible/ck-grok-4.5𝕏

…H_DIR test + align Client-Metadata docs)

# Conflicts:
#	core/providers/gemini-cli/README.md
#	core/providers/gemini-cli/oauth/test_gemini_cli_oauth.py
#	core/src/protocol.rs
#	tui/protocol_fixture_test.go
@phantomic12
phantomic12 force-pushed the feat/gemini-cli-antigravity-oauth branch from bd06b32 to 8abf9e1 Compare August 9, 2026 17:58
catalyst-bot added 3 commits August 9, 2026 14:49
build.sh used to unconditionally pass --features native-browser, which
links the Rust core against libgtk-3, libwebkit2gtk-4.1 and libgio-2.0.
On hosts without those dev headers (headless servers, CI containers,
most developer laptops where the prebuilt bundle is good enough) the
build fails at pkg-config time with no easy workaround.

Add three modes:
  --with-web   force native-browser (was the implicit default)
  --no-web     skip native-browser, build TUI-only core
  (default)    auto-detect via 'pkg-config --exists gio-2.0'

Add docs/build.md covering the Linux apt-get install command, macOS /
Windows notes, and the --no-web workaround for headless hosts. Link to
it from docs/installation.md.

Verified: 'bash build.sh' on a WebKitGTK-less host now builds the
TUI-only core in seconds and replaces the installed catcode/catcode-core
on PATH. 'bash build.sh --with-web' still produces the same pkg-config
error as before, prompting users to install the missing headers.
The core/providers/{antigravity,gemini-cli}/oauth/ test suites run cleanly
locally (14 passing) but were never exercised on CI. Add a workflow that
mirrors the style of ci.yml (permissions: contents: read, cancel-in-
progress concurrency group keyed off the PR number / ref) and runs:

  python3 -m unittest discover -s core/providers -p 'test_*_oauth.py' -v

Triggers on push to main/master and on every pull_request. Uses Python
3.12 on ubuntu-latest; the OAuth helper modules are stdlib-only so no
extra apt or pip install is needed.

Verified locally: 14 tests, 0 failures.
…slugs

The 'Models' section of core/providers/gemini-cli/README.md was claiming
gemini-3-pro-preview, gemini-3-flash-preview, and (by silence) the
sibling Antigravity catalog (claude-*, gpt-oss-*) all work against the
gemini-cli OAuth client. Live HTTP verification (404 from the
cloudcode-pa gateway) shows only the four slugs below actually resolve
on free-tier gemini-cli OAuth; the rest are Antigravity-only.

Narrow the upper 'Models' section to the four verified working slugs:

  gemini-2.5-pro
  gemini-2.5-flash
  gemini-2.5-flash-lite
  gemini-3.1-flash-lite-preview

Add the required note: 'This list was verified live against free-tier
gemini-cli OAuth. The Antigravity bundle has a wider catalog (Claude +
Gemini 3.x + GPT-OSS).' Also call out the Antigravity-only slugs
explicitly and link down to the 'Working models (verified 2026-08)'
section, which keeps the full 404 list for troubleshooting.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — minor suggestions inline.

Reviewed changes Delta since prior pullfrog review (8abf9e191211be): CI for the Python OAuth suites, source-build WebKit flags/docs, and a free-tier-accurate gemini-cli model list.

  • OAuth Python CI — New oauth-python-tests.yml runs unittest discover over core/providers (test_*_oauth.py); 14 tests pass locally with the same command.
  • Source build flagsbuild.sh gains --with-web / --no-web / auto pkg-config gio-2.0 detection, plus docs/build.md and an installation.md link.
  • Gemini model docs — Staged Models section narrowed to free-tier-verified slugs and explicitly marks Antigravity-only IDs as 404 on gemini-cli OAuth.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using openai-compatible/ck-grok-4.5𝕏

Comment thread build.sh Outdated
Comment thread core/providers/gemini-cli/README.md Outdated
@pullfrog

pullfrog Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Addressed both review threads on PR #7 (b9b5ef9):

  1. build.sh auto-detect — Darwin / MINGW / MSYS / CYGWIN now enable native-browser by default; Linux still probes gio-2.0. Updated docs/build.md to match.
  2. gemini-cli Wire sample — model slug changed from gemini-3.1-pro-preview to free-tier-verified gemini-2.5-flash.

Both threads replied and resolved; changes pushed to the PR branch.

Task list (4/4 completed)

Pullfrog  | View workflow run | via Pullfrog | Using openai-compatible/ck-grok-4.5𝕏

pullfrog Bot and others added 2 commits August 10, 2026 19:21
Enable native-browser by default on Darwin/Windows (system WKWebView /
WebView2); keep gio-2.0 probe for Linux. Swap the gemini-cli Wire example
model to a free-tier-verified slug.
Merge origin/master into feat/gemini-cli-antigravity-oauth and close the
three unresolved review threads:

- drop dead Antigravity load_code_assist helper (discover_project_id owns
  the live loadCodeAssist POST path)
- assert expires_at as an inclusive [t0+3600, t1+3600] range in both
  gemini-cli and antigravity token-refresh tests (avoids second-boundary
  flake)
- drop the PR's stale advisor_note/advisor_status no-op + narrow CoreEvent
  variants so master's upsertAdvisor handlers and richer optional fields
  (finding/reason/elapsed_ms) win — unblocks web typecheck CI

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes Delta since prior pullfrog review (b9b5ef9c42d38d): master catch-up merge that closes the remaining open review threads for this PR.

  • Dead helper removed — Antigravity load_code_assist deleted; discover_project_id owns the live :loadCodeAssist POST + extraction path.
  • Token-refresh flake hardening — both provider suites assert expires_at in the inclusive [t0+3600, t1+3600] window instead of a two-point membership check.
  • Web advisor merge — dropped the PR's no-op advisor_note / advisor_status stubs so master's upsertAdvisor handlers and richer optional fields win (unblocks web typecheck).

Prior open threads (platform-aware build.sh, Wire sample model, dead load_code_assist) are addressed on head.

Pullfrog  | View workflow run | Using openai-compatible/ck-grok-4.5𝕏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants