Skip to content

feat(accounts): show Antigravity CLI sign-in and usage - #19588

Closed
artile wants to merge 1 commit into
stablyai:mainfrom
artile:artile/debug-antigravity-accounts
Closed

artile wants to merge 1 commit into
stablyai:mainfrom
artile:artile/debug-antigravity-accounts

Conversation

@artile

@artile artile commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

ELI5

If you are signed into Antigravity CLI (agy) on the computer that runs Orca, Accounts now shows that Google session and the same 5-hour / weekly usage windows as agy /usage. It no longer pretends Antigravity is “Gemini CLI OAuth, experimental toggle off.”

What Changed

  • Read the Antigravity CLI session from ~/.gemini/antigravity-cli (token file + optional email), the same “existing CLI login” pattern as Grok.
  • Refresh a stale access token using the OAuth client embedded in the agy binary (extracted at runtime; secrets are not committed). Concatenated GOCSPX- strings in the binary are split, and every id/secret pair is tried.
  • Fetch quota from daily-cloudcode-pa.googleapis.com retrieveUserQuotaSummary with User-Agent: antigravity. Without that UA, Google returns 403 “valid license of this product”; the production cloudcode-pa host reports fake 0% used.
  • Wire an Antigravity section into Accounts, search, status-bar visibility (antigravityAuthConfigured), and the web stub (unsigned).
  • Stop mirroring Gemini CLI OAuth into the Antigravity usage chip.
  • If quota cannot be read, keep the signed-in card and show the error or unavailable copy instead of an empty refresh.

Why

Accounts never listed Antigravity, so a signed-in agy session looked missing. Status-bar usage was a Gemini OAuth mirror gated on geminiCliOAuthEnabled (off by default). Refresh then failed silently: an expired token plus a glued client secret returned error, and the UI only rendered a usage row for unavailable.

Linked Issue

Fixes #19587

Visual Proof

Before: Accounts showed a signed-in Antigravity card (email) and Refresh usage left no percentage row.

After: Refresh loads named Gemini / Claude+GPT 5-hour and weekly buckets from the daily quota summary (same windows as agy /usage). A failed fetch now shows the error on the card instead of looking like a no-op.

I do not have a committed after screenshot (UI assets must not land in the tree). Repro: Settings → AI Provider Accounts → Antigravity → Refresh usage on a machine with agy signed in.

Testing

  • I manually tested these changes locally (macOS, agy 1.1.27, consumer Google session)
  • Automated tests added/updated, or explained why not below

Targeted vitest: OAuth extractor (concatenated secrets + cartesian pairs), quota-summary parser, usage fetcher (daily host + Antigravity UA + 401 retry), Accounts section (error + buckets), rate-limit service wiring.

SSH / web: usage is read from the Orca runtime host home directory. The web preload stub stays unsigned. WSL home is not scanned (same as Grok). Windows uses where.exe and %LOCALAPPDATA%\agy\bin\agy.exe.

AI Disclosure

Review

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

OAuth client id/secret stay extracted from agy at runtime and are never committed. Quota calls send User-Agent: antigravity because Cloud Code rejects Electron’s Chrome UA with a license 403.

Checklist

  • This PR is small and focused
  • I explained what changed and why (including ELI5)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

Made with Cursor

Read the agy session on disk, refresh its OAuth token from the CLI binary, and fetch daily Cloud Code quota so Accounts and the status bar stop treating Antigravity as a Gemini OAuth mirror.

Co-authored-by: Cursor <cursoragent@cursor.com>
@artile
artile requested a review from brennanb2025 as a code owner September 8, 2026 12:44

@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

antigravity-cli-oauth-extractor.ts imports node:child_process directly, which will fail the child-process-import-boundary ratchet test and block CI. Details inline.

Reviewed changes

  • Antigravity CLI OAuth extraction — new scanner that resolves the agy binary and extracts GOCSPX- secrets and Google client IDs, then tries every id/secret pair to refresh a stale token.
  • Independent quota fetch — reads the agy session file, refreshes via the embedded OAuth client, and parses retrieveUserQuotaSummary from the daily Cloud Code host (with User-Agent: antigravity).
  • Rate-limit service wiring — drops the Gemini mirror in favor of a first-class fetchAntigravityRateLimits() in the fetch cycle, plus an antigravityAuthConfigured flag.
  • Accounts UI + status bar — new AntigravityAccountsSection, search entries, and status-bar visibility now driven by antigravityAuthConfigured instead of a status-bar-item + PATH detection signal.

ℹ️ Nitpicks

  • AntigravityAccountsSection.tsx uses semantic i18n keys (auto.components.settings.AntigravityAccountsSection.title, …) while every sibling section (e.g. GrokAccountsSection) uses the hash-key convention (…GrokAccountsSection.a1b2c3d4e5). Not CI-breaking, but it diverges from the established pattern and the extraction tooling is built around the hash keys.
  • RateLimitBucket in src/shared/rate-limit-types.ts is still documented as "Named per-model buckets (Gemini only)", but Antigravity now populates buckets too.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@@ -0,0 +1,178 @@
import { execFile } from 'node:child_process'

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.

Direct node:child_process import. src/shared/child-process/child-process-import-boundary.test.ts ratchets every direct importer: this file is not in __fixtures__/child-process-import-allowlist.txt, so the has no direct importer outside the allowlist and holds the offender count at the pin assertions both fail in CI. The existing gemini-cli-oauth-extractor.ts is grandfathered into the allowlist; a new sibling is not.

Technical details
# Route the agy binary lookup through the child-process chokepoint

## Affected sites
- src/main/rate-limits/antigravity-cli-oauth-extractor.ts:1 — `import { execFile } from 'node:child_process'` + `promisify(execFile)`
- src/shared/child-process/child-process-import-boundary.test.ts — the ratchet that fails on this import

## Required outcome
- The `which`/`where` probe in `resolveAgyBinary` goes through `runProcess` from `src/shared/child-process/run-process.ts`, which already pins `windowsHide`, `.cmd` resolution, and arg quoting. `runProcess` resolves with `{ code, stdout, stderr, ... }` and never rejects on a non-zero exit (the `which` failure case), so the existing `try/catch` around the lookup can become a plain `code !== 0` / empty-`stdout` check.

## Suggested approach (optional)
- Replace `execFileAsync(lookup, args, { encoding: 'utf-8', windowsHide: true })` with `await runProcess({ file: lookup, args })` and read `result.stdout` (a string). Note `resolveSpawn` encodes `.cmd` args, so keep the win32 `where.exe` path as-is — it is an exe, not a `.cmd`.

home = homedir()
): Promise<void> {
const tokenPath = getAntigravityOAuthTokenPath(home)
const payload: AntigravityOAuthFile = {

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.

saveAntigravityCredentials rebuilds the token file from only the fields Orca models (auth_method, token.{access_token, refresh_token, token_type, expiry}). If the real ~/.gemini/antigravity-cli/antigravity-oauth-token carries anything else — id_token, scope, client_id, or other fields the CLI relies on — a refresh silently strips them and can corrupt the user's agy session. The test only asserts auth_method survives, so this would go unnoticed. Please confirm the agy token schema, or merge the refreshed fields into the existing parsed document (preserving unknown keys) rather than constructing a fresh payload.

@coderabbitai

coderabbitai Bot commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

📝 Walkthrough

Walkthrough

Adds independent Antigravity CLI authentication and quota support. The change reads CLI OAuth sessions, discovers refresh clients from the agy binary, refreshes tokens, and fetches Cloud Code quota summaries. It integrates Antigravity into rate-limit cycles and configuration state. It exposes account status through IPC and preload APIs. The Accounts page displays sign-in status and usage. Status-bar visibility now uses Antigravity authentication directly instead of Gemini CLI OAuth state.

Priority: ➖ Normal — Prioritize the Antigravity CLI integration because it broadly adds account detection, token refresh, quota fetching, IPC, and Accounts/status-bar support across the product.

Severity of issue fixed: Medium

Merge Risk: 🟡 Moderate · up to 09760

The change can expose refreshed credentials to other local users in some environments and can incorrectly report Antigravity quota as unavailable. These issues should be addressed before merge.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.26% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 57 functions across 46 files. (1 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding Antigravity CLI sign-in and usage support.
Description check ✅ Passed The description covers the change, rationale, linked issue, testing, platform considerations, security notes, and checklist. It provides textual visual proof and a reproduction path, although no actua…
Linked Issues check ✅ Passed The implementation addresses issue #19587: it detects Antigravity CLI sessions, displays the signed-in account, fetches 5-hour and weekly quota data with the required endpoint and user agent, refreshe…
Out of Scope Changes check ✅ Passed The changes remain focused on Antigravity account and usage support. OAuth extraction, quota parsing, IPC/preload wiring, UI integration, status-bar behavior, web stubs, and related tests directly sup…
Full details: Docstring Coverage

Explanation

Docstring coverage is 5.26% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 57 functions across 46 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI

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
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 6b7404d7-e662-4f65-b99b-7ee445e83f00

📥 Commits

Reviewing files that changed from the base of the PR and between bba68b1 and 0976023.

📒 Files selected for processing (49)
  • src/main/antigravity-accounts/status.test.ts
  • src/main/antigravity-accounts/status.ts
  • src/main/ipc/antigravity-accounts.ts
  • src/main/ipc/register-core-handlers/register-core-handlers.test.ts
  • src/main/ipc/register-core-handlers/register-core-handlers.ts
  • src/main/rate-limits/antigravity-cli-oauth-extractor.test.ts
  • src/main/rate-limits/antigravity-cli-oauth-extractor.ts
  • src/main/rate-limits/antigravity-oauth-sources.test.ts
  • src/main/rate-limits/antigravity-oauth-sources.ts
  • src/main/rate-limits/antigravity-quota-summary.test.ts
  • src/main/rate-limits/antigravity-quota-summary.ts
  • src/main/rate-limits/antigravity-usage-fetcher.test.ts
  • src/main/rate-limits/antigravity-usage-fetcher.ts
  • src/main/rate-limits/antigravity-usage-mirror.test.ts
  • src/main/rate-limits/antigravity-usage-mirror.ts
  • src/main/rate-limits/rate-limit-service-test-harness.ts
  • src/main/rate-limits/service-account-target-selection.test.ts
  • src/main/rate-limits/service-antigravity-usage.test.ts
  • src/main/rate-limits/service-inactive-account-previews.test.ts
  • src/main/rate-limits/service-live-claude-usage.test.ts
  • src/main/rate-limits/service-minimax-usage.test.ts
  • src/main/rate-limits/service-refresh-orchestration.test.ts
  • src/main/rate-limits/service-window-activation.test.ts
  • src/main/rate-limits/service/service-configuration.ts
  • src/main/rate-limits/service/service-full-cycle-application.ts
  • src/main/rate-limits/service/service-full-cycle-preparation.ts
  • src/main/rate-limits/service/service-state.ts
  • src/preload/api-types.ts
  • src/preload/api/agent-account-api.ts
  • src/preload/api/antigravity-accounts-bridge.ts
  • src/preload/index.ts
  • src/renderer/src/components/settings/AccountsPane.test.tsx
  • src/renderer/src/components/settings/AccountsPane.tsx
  • src/renderer/src/components/settings/AntigravityAccountsSection.test.tsx
  • src/renderer/src/components/settings/AntigravityAccountsSection.tsx
  • src/renderer/src/components/settings/accounts-search.test.ts
  • src/renderer/src/components/settings/accounts-search.ts
  • src/renderer/src/components/status-bar/status-bar-provider-visibility.test.ts
  • src/renderer/src/components/status-bar/status-bar-provider-visibility.ts
  • src/renderer/src/components/status-bar/usage-provider-settings-target.test.ts
  • src/renderer/src/components/status-bar/usage-provider-settings-target.ts
  • src/renderer/src/components/status-bar/use-status-bar-controller.ts
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/web/preload-api/web-agent-accounts-api.ts
  • src/renderer/src/web/web-preload-api-composition.test.ts
  • src/renderer/src/web/web-preload-api.ts
  • src/shared/rate-limit-state-factory.ts
  • src/shared/rate-limit-types.test.ts
  • src/shared/rate-limit-types.ts
💤 Files with no reviewable changes (2)
  • src/main/rate-limits/antigravity-usage-mirror.test.ts
  • src/main/rate-limits/antigravity-usage-mirror.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.

}
}
const tmpPath = `${tokenPath}.${process.pid}.tmp`
await writeFile(tmpPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf-8')

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.

🔒 Security & Privacy | 🛡️ Analyzed with Security Review | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

node --version
tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT

(
  umask 000
  TMP_DIR="$tmp_dir" node <<'NODE'
const { rename, stat, writeFile } = require('node:fs/promises')
const path = require('node:path')

async function main() {
  const temporary = path.join(process.env.TMP_DIR, 'antigravity-oauth-token.1.tmp')
  const target = path.join(process.env.TMP_DIR, 'antigravity-oauth-token')
  await writeFile(temporary, '{"token":{"refresh_token":"secret"}}\n', 'utf8')
  await rename(temporary, target)
  const mode = (await stat(target)).mode & 0o777
  console.log(`resulting mode: ${mode.toString(8)}`)
  if ((mode & 0o077) !== 0) process.exit(1)
}

main()
NODE
)

Repository: stablyai/orca

Length of output: 180


Sensitive Data Exposure (CWE-522): Insufficiently Protected Credentials

Reachability: Internal · Exploitability: Moderate

Restrict permissions on the persisted OAuth token file.

writeFile can create tmpPath with mode 0o666 before the process umask. rename preserves that mode. Create the temporary file with mode 0o600.

Proposed fix
-  await writeFile(tmpPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf-8')
+  await writeFile(tmpPath, `${JSON.stringify(payload, null, 2)}\n`, {
+    encoding: 'utf-8',
+    mode: 0o600
+  })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await writeFile(tmpPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf-8')
await writeFile(tmpPath, `${JSON.stringify(payload, null, 2)}\n`, {
encoding: 'utf-8',
mode: 0o600
})

Comment on lines +55 to +71
async function fetchQuotaSummary(accessToken: string, projectId: string | null): Promise<Response> {
const body = JSON.stringify(projectId ? { project: projectId } : {})
let last: Response | undefined
for (const url of QUOTA_SUMMARY_URLS) {
const response = await net.fetch(url, {
method: 'POST',
headers: quotaHeaders(accessToken),
body,
signal: AbortSignal.timeout(API_TIMEOUT_MS)
})
last = response
if (response.ok || response.status === 401 || response.status < 500) {
return response
}
}
return last!
}

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Continue after a 404 from the primary quota endpoint.

response.status < 500 matches 404, so fetchQuotaSummary stops before trying the sandbox compatibility endpoint. Continue for 404 responses while preserving terminal 401/403 authentication and license responses.

@nwparker

Copy link
Copy Markdown
Contributor

Review of head 0976023cd0e8393619e1677e8c56bf6781b84a8f: I found credential-save blockers and am not adopting this head unchanged.

I compiled the candidate's actual antigravity-oauth-sources.ts and ran saveAntigravityCredentials against a temporary directory containing synthetic placeholder credentials only. No real token file, binary OAuth client material, account, or login was modified.

  1. Saving broadens POSIX file permissions. A token file created with mode 0600 became 0644 after save under a normal 0022 umask. writeFile(tmpPath, ..., 'utf-8') creates the replacement with default permissions, then rename replaces the previously private file. The refreshed access/refresh tokens become readable by other local users who can traverse the parent directories.
  2. A stale refresh can overwrite a new login. I read account A's fixture, replaced the on-disk file with account B's fixture, then saved A's refresh result. The result contained A's refresh token again. The production caller awaits network refresh and unconditionally invokes this save function, without checking whether the source account/token changed during that await.
  3. The rewrite drops unrecognized provider fields. An extra fixture metadata field disappeared because the save reconstructs a limited schema rather than preserving the latest provider-owned document.

ELI5: refreshing the usage card must not make agy's login file less private or undo an account switch the user just made.

A direct credential-writing implementation needs private temporary-file permissions, platform-appropriate access handling, preservation of provider fields, and a freshness/identity check that does not overwrite a newer login. Prefer avoiding ownership of agy's refresh file for quota: #21654 already reads the supported native /usage command, leaving token refresh with agy. It does not yet solve account identity or multi-account management.

This machine currently uses API-key authentication and has no token file at the candidate's expected location. Native /usage works but returns no quota groups here; a signed-in Google card and populated quota cannot be validated from that evidence. OAuth identity, account switching, Windows ACL behavior, and SSH/WSL account scope still need live verification. No merge or issue closure.

@nwparker

Copy link
Copy Markdown
Contributor

Review update: this PR is superseded by #21784 for native macOS account discovery and switching.

The older implementation assumes a plain Antigravity token file and extracts OAuth client secrets from the agy binary. Current installed agy evidence shows the native macOS credential is stored in the OS Keychain as a go-keyring-base64: blob, so that backend would not reliably read the signed-in account and would broaden secret-handling risk. #21784 preserves the opaque native blob, uses /usr/bin/security, validates readback, and keeps account vault data encrypted.

Scoped verdict: do not merge this head. Keep the PR open only if the author wants to rebase the quota work onto the native credential backend; otherwise it should be closed as superseded after human confirmation.

@nwparker

nwparker commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Superseded after #24691 and #24593 merged. Thanks @artile: the signed-in Accounts card, quota/error visibility and independence from Gemini are preserved and explicitly credited in the replacements. The original credential-rewriting approach was not merged. Broader runtime/platform acceptance remains in #19587, and persistent multi-account work remains in #17345; #23761 stays open for its separate OpenCode catalog contribution.

@nwparker nwparker closed this Oct 4, 2026
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.

[Feature]: Show Antigravity CLI sign-in and usage in Accounts

2 participants