# Antigravity Auth

Google Antigravity OAuth for coding agents. Authenticate with your Google account and access Antigravity quota for Gemini, Claude, and GPT-OSS models from OpenCode, the Pi coding agent, or the standalone CLI.

This monorepo ships three packages:

| Package | Host | Role |
| --- | --- | --- |
| [`@cortexkit/opencode-antigravity-auth`](packages/opencode) | OpenCode 1.x server | Intercepts `fetch()`, runs the account pool + quota manager, drives slash commands, and exposes a TUI sidebar through a loopback RPC. |
| [`@cortexkit/pi-antigravity-auth`](packages/pi) | Pi coding agent | Registers a custom provider with OAuth login + Gemini streaming. |
| [`@cortexkit/antigravity-auth-core`](packages/core) | Any harness | Harness-agnostic core: OAuth PKCE, raw HTTP/1.1 transport, device fingerprint, request transforms, account pool, quota manager, durable storage. Both host packages depend on it. |

## Risk and terms-of-service warning

> **CAUTION — read before installing.**
>
> Using this software (and any proxy for Antigravity) violates Google's Terms of Service. Google account holders have reported **suspensions, bans, and shadow-bans** (restricted access without notice). The tool is **not endorsed by Google**, and every account is operated by the account holder. You assume all legal, financial, and technical risk. Operators and contributors are not liable for outcomes.

The shipped headers, project IDs, and OAuth client identifiers match the publicly observable behavior of the upstream Antigravity CLI; they are not credentials and may be inspected in this repository. **OAuth credentials (refresh tokens, access tokens, browser cookies), bearer tokens, and request/response dumps never appear in this README or the project tree** — they live exclusively in `antigravity-accounts.json` on the operator's machine, the `~/.cache/opencode/antigravity-accounts.json` fallback, or the developer-controlled debug log directory. Do not paste any of them into issues or chat.

## Packages and supported hosts

| Host | Package version | Host requirement | Install path |
| --- | --- | --- | --- |
| OpenCode (server + TUI) | `2.0.0` | `@opencode-ai/plugin >=1.17.13 <2` · `@opentui/core`, `@opentui/keymap`, `@opentui/solid` all `^0.4.5` | `opencode plugin @cortexkit/opencode-antigravity-auth@latest` (writes the server entry to `opencode.json` and the TUI entry to `tui.json`) |
| Pi coding agent | `2.0.0` | `@earendil-works/pi-ai`, `@earendil-works/pi-coding-agent`, `@earendil-works/pi-tui` (peer) | `pi install npm:@cortexkit/pi-antigravity-auth` then `/login google-antigravity` |
| Standalone CLI | shipped inside the OpenCode package as the `antigravity-auth` bin | Node 20+ | `npx @cortexkit/opencode-antigravity-auth` |
| Node libraries (harness builders) | `2.0.0` core | Node 20+ | `npm install @cortexkit/antigravity-auth-core` |

The OpenCode package exposes two `exports` subpaths the host installer reads:

- `exports["."]` — the bundled `dist/index.js` server root; `opencode plugin` writes this entry into `opencode.json`'s `plugin` array.
- `exports["./tui"]` — `src/tui/entry.mjs`, the OpenTUI sidebar loader; `opencode plugin` writes this entry into `tui.json` so the TUI process picks it up on the next host start. The precompiled JSX tree ships under `dist/src/tui-compiled/` and is regenerated by `bun run build:tui`; the host's loader also accepts the raw `src/tui.tsx` source path for development installs.

The host enforces the minimum OpenCode version through the package's `engines.opencode` field (`>=1.17.13 <2`); `opencode plugin` refuses to install a plugin that asks for an unsupported range. Both core and opencode packages are dual-target: the OpenCode package bundles a CommonJS-free ESM artifact via `esbuild`, while the core package ships `tsc`-emitted ESM. Runtime requirements match the `engines.node` field (`>=20`).

## Installation matrix

### OpenCode (npm — the only end-user install path)

The supported installer writes both registrations in one step:

```bash
opencode plugin @cortexkit/opencode-antigravity-auth@latest
```

What this does:

- Reads the package's `exports["."]` (the server root) and writes it into `~/.config/opencode/opencode.json` under the `plugin` array — that is the `auth.loader` and `fetch` interceptor the host calls on every model dispatch.
- Reads the package's `exports["./tui"]` (the OpenTUI sidebar loader) and writes it into `~/.config/opencode/tui.json` so the TUI process picks the sidebar up on the next host start.
- Refuses to install when the host's OpenCode version falls outside the package's `engines.opencode` range (`>=1.17.13 <2`).

#### Manual config (hand-edited configs)

If you cannot use `opencode plugin`, write both files by hand. The server entry lives in `opencode.json`:

```jsonc
// ~/.config/opencode/opencode.json
{
  "plugin": ["@cortexkit/opencode-antigravity-auth@latest"]
}
```

The TUI entry lives in a separate `tui.json` file under the same config dir:

```jsonc
// ~/.config/opencode/tui.json
{
  "plugin": ["@cortexkit/opencode-antigravity-auth"]
}
```

The two files are independent — the host reads the server registration from `opencode.json` and the TUI registration from `tui.json`. Dropping one side disables that half of the plugin without touching the other.

### OpenCode from this checkout (contributors)

Link the package into your local OpenCode config so the working-tree version is used:

```bash
bun install                                  # at repo root
bun run build                                # build core, opencode, pi in order
bun run --cwd packages/opencode smoke:tui    # smoke-test the bundled TUI
```

Then point `OPENCODE_CONFIG_DIR` at a project that has the development plugin entry, or copy `dist/` and `dist/src/tui-compiled/` into a directory OpenCode watches.

### Pi

```bash
pi install npm:@cortexkit/pi-antigravity-auth
pi                                                # interactive
/login google-antigravity                         # OAuth
```

Pi's extension loader discovers the package's `pi.extensions` field and registers the provider automatically; no manual wiring required.

### Standalone CLI

The `antigravity-auth` bin lives inside the OpenCode package; no separate install is needed:

```bash
npx -y @cortexkit/opencode-antigravity-auth login        # OAuth flow, no plugin
npx -y @cortexkit/opencode-antigravity-auth list         # print accounts
npx -y @cortexkit/opencode-antigravity-auth quota        # live quota snapshot
npx -y @cortexkit/opencode-antigravity-auth quota --refresh --json
```

The CLI writes to the same `antigravity-accounts.json` the OpenCode plugin reads, so a `login` from the standalone bin is visible to the plugin immediately on the next restart.

## First login

1. Install the package and start OpenCode (or the standalone CLI).
2. For OpenCode: open the auth menu and choose **Antigravity (Google OAuth)**, or run `/antigravity-account add`.
3. For the CLI: run `antigravity-auth login`. `--project <id>` pins a Google Cloud project before the flow; `--no-browser` prints the URL instead of opening it (useful in headless / SSH sessions — see [Troubleshooting](#troubleshooting)).
4. Approve the consent screen. The browser redirects to `http://localhost:51121/oauth-callback`; the local listener completes the PKCE exchange, persists `refreshToken` / `accessToken` / `email` / optional `projectId` into `~/.config/opencode/antigravity-accounts.json` (mode `0600`).
5. Verify with `antigravity-auth quota`.

A fresh install may auto-fetch a project ID from the Cloud Code Assist API. If the lookup fails the plugin uses the hardcoded fallback project (`rising-fact-p41fc`); see [403 Permission Denied](https://github.com/cortexkit/antigravity-auth/issues) for manually setting a project ID.

## Models and routing

`packages/core/src/model-registry.ts` is the canonical source of truth. The OpenCode package re-exports it as `getAntigravityOpencodeModelIds()` (consumed by `applyAntigravityProviderCatalog`).

| Family | Header style | Quota group | Routing |
| --- | --- | --- | --- |
| Claude (Opus / Sonnet thinking) | `antigravity` | `claude` | Antigravity only |
| Gemini 3.x / 2.5 (Pro / Flash / Image) | `antigravity` then `gemini-cli` | `gemini-pro`, `gemini-flash` | Antigravity first; `cli_first: true` reverses it |
| GPT-OSS 120B | `antigravity` | `gpt-oss` | Antigravity only |

Header style controls the `User-Agent`, `X-Goog-Api-Client`, and `Client-Metadata` triplets that the request transform attaches. The interceptor picks style per request:

- **Antigravity-first (default):** Gemini requests first try the Antigravity header set; on `429 RESOURCE_EXHAUSTED` they fall back to the Gemini CLI header set.
- **CLI-first (`cli_first: true`):** Gemini requests first try the Gemini CLI header set; on `429` they fall back to Antigravity.
- **Style-fallback (`quota_style_fallback: true`):** Re-sends the SAME request through the other header set when the active pool is rate-limited, consuming tokens from BOTH pools (default off to prevent double-spend across pools). The field is mutable from the `/antigravity-routing` dialog at runtime — toggling it does not require a restart.
- **Claude always uses Antigravity** regardless of the toggles.

When all accounts for a given family cool down, the fetch interceptor waits up to `max_rate_limit_wait_seconds` (default `300`) before failing fast.

### Model IDs shipped in the registry

- `antigravity-gemini-3-pro`, `antigravity-gemini-3-flash`, `antigravity-gemini-3.5-flash`, `antigravity-gemini-3.1-pro`, `antigravity-gemini-3.1-flash-image`
- `antigravity-claude-sonnet-4-6`, `antigravity-claude-opus-4-6-thinking`
- `antigravity-gpt-oss-120b-medium`
- `gemini-2.5-flash`, `gemini-2.5-pro`, `gemini-3-flash-preview`, `gemini-3-pro-preview`, `gemini-3.1-pro-preview`, `gemini-3.1-pro-preview-customtools` (Gemini CLI namespace)

Legacy `antigravity-gemini-3-flash` style aliases resolve through `MODEL_ALIASES` in the resolver; new model names should be added through the registry, not hand-coded in the transforms. The Transform layer renames old → new automatically.

## Multi-account behavior

The account pool persists in `antigravity-accounts.json` (storage schema `AccountStorageV4`) with locking via `acquireFencedFileLock` so concurrent writers cannot corrupt the file. Each account carries:

- `refreshToken` (stored in plaintext; the file sits under `0600` with its parent directory at `0700` — filesystem permissions are the only protection at rest)
- `email`, `enabled`, `coolingDownUntil`, `cachedQuota`, `healthScore`
- `projectId` (optional; used by Gemini CLI)
- `fingerprintHistory` and `verificationRequired` / `verificationUrl` (used by the verification flow)

Account selection (`account_selection_strategy`):

| Strategy | Behavior |
| --- | --- |
| `sticky` | Use one account until it is rate-limited; preserves Anthropic's prompt cache. Recommended for single-session use. |
| `hybrid` (default) | Token bucket + health score + LRU freshness. Best for most workloads. |
| `round-robin` | Rotate per request. Useful when running many short requests in parallel. |

`pid_offset_enabled` shifts the per-session starting index so different PIDs begin at different accounts — recommended when running multiple parallel subagents and `oh-my-opencode` is installed. Per-request caps: `max_account_switches` (default `10`) bounds how many accounts a single request may try before failing.

`switch_on_first_rate_limit` controls whether the interceptor switches accounts after the first 429 (default on) or retries the same account once first. `default_retry_after_seconds` (60) and `max_backoff_seconds` (60) shape the wait between attempts. `request_jitter_max_ms` adds timing jitter — set to `100–500` to avoid thundering-herd patterns when running many parallel agents.

## Quota semantics and killswitch

Each account carries a per-quota-group `cachedQuota` (`{ remainingFraction, resetTime }` for `claude`, `gemini-pro`, `gemini-flash`, `gpt-oss`). The quota manager refreshes them opportunistically:

- **Periodic refresh:** every `quota_refresh_interval_minutes` after a successful request (default `30`; the schema migrated from 15 to 30 for v2 — set to `0` to disable).
- **TTL:** `soft_quota_cache_ttl_minutes = "auto"` resolves to `max(2 × refresh_interval, 10)`. While the cache is fresh the threshold check is authoritative.
- **Stale-TTL fail-open:** when the cache is older than the TTL OR for an account the plugin has never refreshed, the account is allowed through the soft-quota check. A cold start can never deadlock on the first dial.
- **Soft-quota threshold:** `soft_quota_threshold_percent` (default `80`) — accounts with usage ≥ threshold are skipped as if rate-limited.
- **All-exhausted:** when every account exceeds the threshold the interceptor waits for the earliest reset time, then waits up to `max_rate_limit_wait_seconds` before failing fast.
- **Proactive rotation:** after a successful request, if the active account's remaining quota drops below `proactive_rotation_threshold_percent` (default `20`) the next request is dispatched from a warm-cache account to avoid a forced 429 mid-conversation.

The **operator killswitch** (`/antigravity-killswitch`) is a hard rejection layer run before dispatch:

- `enabled` (default `false`) — master switch.
- `minimum_remaining_percent` — global threshold; accounts whose freshest cached quota is ≤ this are excluded.
- `accounts` — per-account override keyed by `sha256(refreshToken).slice(0,12)`. The key never appears outside the config file; no raw token is ever written, logged, or sent over RPC.
- **Fail-open on stale/missing cache:** a candidate with no fresh cached quota (TTL `5min` by default) is allowed through so the first dispatch after a cold start cannot deadlock.
- **All-killed error:** when the entire pool is excluded, the interceptor throws `AntigravityKillswitchError` carrying a per-account summary so the host can surface a single, structured error rather than a synthetic body.

Both filters are evaluated *before* the token bucket / health score pick, so the operator has the last word even on a healthy pool.

## OpenCode sidebar and slash commands

The sidebar lives under the OpenCode TUI; it is **read-only** and polls `sidebar-state.json` every two seconds. The state contract version is `1`; the host loader silently falls back to `DEFAULT_SIDEBAR_STATE` on malformed JSON or unknown versions.

### Slash commands

All six commands are registered in `packages/opencode/src/plugin/commands.ts` and the modal flow goes through `packages/opencode/src/tui/command-dialogs.tsx`. The plugin entry writes a sidebar freshness bump after every apply through `createSidebarRefresher` so the TUI's next poll sees a fresh `checkedAt`.

| Command | Args | Effect |
| --- | --- | --- |
| `/antigravity-quota` | `[refresh]` | Status (or refresh) of cached quota across accounts. Returns within `2s` default. |
| `/antigravity-account` | `add` · `refresh` · `remove` · `list` | Manage the account pool. `add` and `refresh` opt into a `120_000` ms RPC timeout (OAuth can take two minutes on a fresh login). `list` and `remove` use the `2_000` default. |
| `/antigravity-routing` | `cli_first=true\|false` · `quota_style_fallback=true\|false` | Toggle the live routing flags. Omit a key to flip the current value. Persists immediately. |
| `/antigravity-killswitch` | `enabled=true\|false` · `minimum_remaining_percent=0..100` | Configure the killswitch. Per-account thresholds are set via direct config edit. |
| `/antigravity-dump` | `enable` · `disable` · `status` | Toggle wire-dump capture for the next request. (Backward-compatible alias: `/gemini-dump`.) |
| `/antigravity-logging` | `error` · `warn` · `info` · `debug` · `trace` | Adjust runtime logging; takes effect immediately and persists across restarts. |

Every apply RPC defaults to `2_000` ms. The account `add` / `refresh` paths opt into `120_000` ms because the OAuth flow can take up to two minutes on a fresh login. The two timeouts are visible to operators as the `timeoutMs` knob on the apply result.

### Sidebar contract

`getSidebarStateFile()` resolves to `$XDG_STATE_HOME/cortexkit/antigravity-auth/sidebar-state.json` (or `~/.local/state/...`). Override with `ANTIGRAVITY_AUTH_SIDEBAR_STATE_FILE`. The file carries:

- `version: 1`, `checkedAt` (epoch ms), `accounts[]`, `activeRouting` (per-session map, pruned to 24h / 100 entries), `routingAuthoritative`, optional `quotaBackoffUntil` and `lastError`.

Every writer follows the same recipe (serialize through `sidebarWriteChain` → acquire fenced file lock with `2s` retry → re-read and merge → atomic rename mode `0600`). If a cross-process lock holder does not release within `SIDEBAR_LOCK_TIMEOUT_MS` (`2_000` ms) the writer surfaces `SidebarStateLockContentionError`; the caller decides whether to surface a toast, drop the write, or retry next tick.

## Standalone CLI

`antigravity-auth` (the bin inside the OpenCode package) accepts three commands. The parser rejects anything else with `exit 2`.

```bash
antigravity-auth login [--project <id>] [--no-browser]
antigravity-auth list [--json]
antigravity-auth quota [--json] [--refresh]
```

- `login` performs the same PKCE + local callback flow as the plugin. `--project <id>` pins the project ID before the request; `--no-browser` prints the URL instead of opening it. The CLI auto-detects a headless session (`SSH_*`, `OPENCODE_HEADLESS=1`) and combines that with `--no-browser` for you.
- `list` prints a fixed-width table (`INDEX`, `EMAIL`, `STATUS`) where `STATUS` is `active`, `disabled`, `ineligible`, or `verification-required`. `--json` emits the same data as JSON.
- `quota` prints `ACCOUNT`, `STATUS`, `GROUP`, `REMAINING`, `RESET`. With `--refresh` it forces a live refresh; with `--json` it emits the same array as JSON. Exit code is `0` on success, `1` on a thrown error, and `2` on a parse failure.

All commands write to the same `antigravity-accounts.json` the plugin reads; a login from the CLI shows up in the next sidebar poll after the plugin reload.

## Pi usage

```bash
pi install npm:@cortexkit/pi-antigravity-auth
pi
/login google-antigravity                    # OAuth flow
```

The Pi extension registers the `google-antigravity` provider automatically through the package's `pi.extensions` field; no further wiring is needed. Both packages delegate to `@cortexkit/antigravity-auth-core`, so the request transport and model transforms are shared. The account-pool layer is not: Pi holds a single credential locally (no `antigravity-accounts.json`, no rotation, no killswitch, no operator settings). Multi-account rotation, quota routing, and the killswitch are OpenCode-only.

## Configuration reference

`packages/opencode/src/plugin/config/schema.ts` defines `AntigravityConfigSchema` (Zod); the generated JSON Schema ships at `packages/opencode/assets/antigravity.schema.json`. Every nested config property, default, and environment override is documented below. **Project** settings (`.opencode/antigravity.json`) override **user** settings (`~/.config/opencode/antigravity.json`), which in turn override env vars.

| Path | Type | Default | Env override | Effect |
| --- | --- | --- | --- | --- |
| `quiet_mode` | bool | `false` | `OPENCODE_ANTIGRAVITY_QUIET=1` | Suppress rate-limit and account-switching toasts; recovery toasts always shown. |
| `toast_scope` | `root_only` \| `all` | `root_only` | `OPENCODE_ANTIGRAVITY_TOAST_SCOPE=all` | `root_only` silences subagents; `all` is verbose. |
| `debug` | bool | `false` | `OPENCODE_ANTIGRAVITY_DEBUG=1` | File logging to `~/.config/opencode/antigravity-logs/`. |
| `debug_tui` | bool | `false` | `OPENCODE_ANTIGRAVITY_DEBUG_TUI=1` | TUI log-panel verbosity (independent of `debug`). |
| `log_dir` | string | XDG-derived | `OPENCODE_ANTIGRAVITY_LOG_DIR=…` | Custom debug-log directory. |
| `keep_thinking` | bool | `false` | `OPENCODE_ANTIGRAVITY_KEEP_THINKING=1` | Preserve Claude thinking blocks (signature caching); can destabilize models. |
| `thinking_warmup` | bool | `false` | `OPENCODE_ANTIGRAVITY_THINKING_WARMUP=1` | Costly warmup probe at session start. |
| `cache_warmup_on_switch` | bool | `true` | `OPENCODE_ANTIGRAVITY_CACHE_WARMUP_ON_SWITCH=1` | Cache-seeding probe after account rotation. |
| `session_recovery` | bool | `true` | — | Auto-recover from `tool_result_missing` errors. |
| `auto_resume` | bool | `true` | — | Send `resume_text` after recovery. |
| `resume_text` | string | `"continue"` | — | What to send on auto-resume. |
| `signature_cache.enabled` | bool | `true` | — | Disk-persist thinking-block signatures. |
| `signature_cache.memory_ttl_seconds` | number (60-86400) | `3600` | — | In-memory signature TTL. |
| `signature_cache.disk_ttl_seconds` | number (3600-604800) | `172800` | — | On-disk signature TTL. |
| `signature_cache.write_interval_seconds` | number (10-600) | `60` | — | Background flush interval. |
| `empty_response_max_attempts` | number (1-10) | `2` | — | Retries when Antigravity returns no candidates. |
| `empty_response_retry_delay_ms` | number (500-10000) | `2000` | — | Delay between empty-response retries. |
| `tool_id_recovery` | bool | `true` | — | Match orphan tool IDs by name. |
| `claude_tool_hardening` | bool | `true` | — | Inject strict tool-usage rules for Claude. |
| `claude_prompt_auto_caching` | bool | `false` | — | Add top-level `cache_control` when absent. |
| `proactive_token_refresh` | bool | `true` | — | Refresh access tokens before expiry. |
| `proactive_refresh_buffer_seconds` | number (60-7200) | `1800` | — | Pre-expiry refresh window. |
| `proactive_refresh_check_interval_seconds` | number (30-1800) | `300` | — | Refresh-check cadence. |
| `max_rate_limit_wait_seconds` | number (0-3600) | `300` | — | Cap on queueing wait; `0` waits indefinitely. |
| `quota_fallback` | bool | `false` | — | **Deprecated**, ignored at runtime; kept for back-compat. |
| `cli_first` | bool | `false` | — | CLI header first instead of Antigravity for Gemini. |
| `account_selection_strategy` | `sticky` \| `round-robin` \| `hybrid` | `hybrid` | `OPENCODE_ANTIGRAVITY_ACCOUNT_SELECTION_STRATEGY` | Account picker. |
| `pid_offset_enabled` | bool | `false` | `OPENCODE_ANTIGRAVITY_PID_OFFSET_ENABLED=1` | Per-PID starting account index. |
| `switch_on_first_rate_limit` | bool | `true` | — | Switch immediately on first 429. |
| `max_account_switches` | number (0-500) | `10` | `OPENCODE_ANTIGRAVITY_MAX_ACCOUNT_SWITCHES` | Account-switch budget per request. |
| `quota_style_fallback` | bool | `false` | `OPENCODE_ANTIGRAVITY_QUOTA_STYLE_FALLBACK` | Re-send through the other header pool on exhaustion. |
| `scheduling_mode` | `cache_first` \| `balance` \| `performance_first` | `cache_first` | `OPENCODE_ANTIGRAVITY_SCHEDULING_MODE` | Rate-limit scheduling strategy. |
| `max_cache_first_wait_seconds` | number (5-300) | `60` | — | Wait cap when `scheduling_mode=cache_first`. |
| `failure_ttl_seconds` | number (60-7200) | `3600` | — | Failure-count reset window. |
| `default_retry_after_seconds` | number (1-300) | `60` | — | Fallback retry delay when 429 omits `Retry-After`. |
| `max_backoff_seconds` | number (5-300) | `60` | — | Exponential backoff cap. |
| `request_jitter_max_ms` | number (0-5000) | `0` | — | Random pre-request delay to break cadence. |
| `switch_account_delay_ms` | number (0-10000) | `500` | — | Delay before switching accounts. |
| `soft_quota_threshold_percent` | number (1-100) | `80` | — | Skip accounts above this usage %; `100` disables. |
| `quota_refresh_interval_minutes` | number (0-120) | `30` | — | Background refresh cadence; `0` disables. |
| `soft_quota_cache_ttl_minutes` | `auto` \| number (1-120) | `"auto"` | — | TTL for cached quota. `auto` = `max(2 × refresh, 10)` minutes. |
| `proactive_rotation_threshold_percent` | number (0-100) | `20` | `OPENCODE_ANTIGRAVITY_PROACTIVE_ROTATION_THRESHOLD` | Proactive account switcher; `0` disables. |
| `health_score.initial` | number (0-100) | `70` | — | Health-score starting value. |
| `health_score.success_reward` | number (0-10) | `1` | — | Score gain per success. |
| `health_score.rate_limit_penalty` | number (-50..0) | `-10` | — | Penalty per rate limit. |
| `health_score.failure_penalty` | number (-100..0) | `-20` | — | Penalty per failure. |
| `health_score.recovery_rate_per_hour` | number (0-20) | `2` | — | Slow heal over time. |
| `health_score.min_usable` | number (0-100) | `50` | — | Account is gated below this. |
| `health_score.max_score` | number (50-100) | `100` | — | Score ceiling. |
| `token_bucket.max_tokens` | number (1-1000) | `50` | — | Max concurrent budget. |
| `token_bucket.regeneration_rate_per_minute` | number (0.1-60) | `6` | — | Rate budget refills. |
| `token_bucket.initial_tokens` | number (1-1000) | `50` | — | Starting budget. |
| `auto_update` | bool | `true` | — | Background npm update check + auto-pin. |
| `operator.routing.cli_first` | bool | `false` | — | Live override of `cli_first` (from `/antigravity-routing`). |
| `operator.routing.quota_style_fallback` | bool | `false` | — | Live override of `quota_style_fallback`. |
| `operator.killswitch.enabled` | bool | `false` | — | Master killswitch. |
| `operator.killswitch.minimum_remaining_percent` | number (0-100) | `5` | — | Global hard block threshold. |
| `operator.killswitch.accounts` | record<string, number> | unset | — | Optional per-account override (key = `sha256(refreshToken).slice(0,12)`). Absent by default; falls back to `minimum_remaining_percent`. |
| `operator.log_level` | `error` \| `warn` \| `info` \| `debug` \| `trace` | `"info"` | — | Runtime log-level filter (mutable from `/antigravity-logging`). |

`globalThis.fetch` calls inside the package go through `fetchWithActiveTimeout` (see [State, cache, log, RPC, and dump files](#state-cache-log-rpc-and-dump-files)).

## State, cache, log, RPC, and dump files

| Purpose | Path | Mode | Notes |
| --- | --- | --- | --- |
| Account pool | `$OPENCODE_CONFIG_DIR/antigravity-accounts.json` (or `$XDG_CONFIG_HOME/opencode/antigravity-accounts.json`, fallback to `%APPDATA%\opencode\…`) | `0600` | Per-account tokens, quota cache, fingerprint history. Storage schema `V4`. |
| Plugin config | `antigravity.json` (project `.opencode/`, then user) | `0600` | The Zod-validated config above; `OpenCodeConfigDir` environment variable override. |
| Debug logs | `antigravity-logs/<file>.log` (or `log_dir`) | `0600` | Created by the debug subsystem when `debug` is on. |
| Sidebar state | `$XDG_STATE_HOME/cortexkit/antigravity-auth/sidebar-state.json` (override `ANTIGRAVITY_AUTH_SIDEBAR_STATE_FILE`) | `0600` | Versioned contract `1`; pruned to 24h/100 entries. |
| RPC port file | `$XDG_STATE_HOME/cortexkit/antigravity-auth/rpc/<project-hash>/port-<pid>.json` (override `ANTIGRAVITY_AUTH_RPC_DIR`) | `0600` dir `0700` | Loopback HTTP token bearer for the TUI. |
| Signature cache | `signature-cache-disk-<hash>.json` under `keep_thinking` path | `0600` | 2-day default TTL. |
| Wire dump | `antigravity-logs/dumps/` | `0600` | Sensitive — see [Security and redaction](#security-and-redaction). |

The **fetch interceptor** uses `fetchWithActiveTimeout` (`packages/core/src/fetch-timeout.ts`). The contract is intentional: the **`15_000` ms header-timeout** aborts only the request until the underlying `fetch` resolves; once headers arrive the timeout listener is removed and the body may stream past the deadline. **Streaming responses are bounded by the `agy-transport.ts` idle-timeout watchdog (`180_000` ms / 3 min)**, so a stalled SSE chunk counts as a transport failure even though the header budget has been exceeded. Operators should not raise the 15s header window to chase a streaming response — the stream watchdog is the right knob.

The **RPC** between the TUI (`packages/opencode/src/tui-compiled/`) and the server plugin (`packages/opencode/src/rpc/rpc-server.ts`) uses a per-PID port file (`$XDG_STATE_HOME/cortexkit/antigravity-auth/rpc/<project-hash>/port-<pid>.json`). The TUI process reads the highest-mtime live entry, drops stale entries with `process.kill(pid, 0)` checks, and posts to `/rpc/apply` or `/rpc/pending-notifications` with a `Bearer <token>` header. Apply default timeout is `2_000` ms; `120_000` ms for `antigravity-account add`/`refresh`. Notifications are pushed over loopback HTTP and drained via the same `/rpc/pending-notifications` long-poll.

**Disposal:** the plugin's `dispose()` (called on host shutdown) drains sidebar writes via `drainSidebarWrites()` BEFORE tearing down the RPC server and file logger so any in-flight write enqueued at shutdown lands before the host closes the terminal. The lifecycle disposes (in order): the active `AccountManager` + token refresh queue → the disk signature cache → the session registry → the fetch state → sidebar drain → registered disposables (RPC server, file logger).

## Security and redaction

- Refresh tokens live in `antigravity-accounts.json` with file mode `0600`. The file is rewritten through `acquireFencedFileLock` so concurrent writers never corrupt state. Existing overly-permissive files are tightened on load.
- The sidebar state file carries only `id`/`label`/`enabled`/`health`/`current`/`cooldownUntil`/`quota` — never a refresh token, access token, project ID, fingerprint, or other credential-shaped field. `redactAccountForSidebar` is the single conversion point.
- The killswitch `accounts` field is keyed by `sha256(refreshToken).slice(0,12)` so the raw token never lands in the config, sidebar, RPC payload, or apply response. The hash is deterministic per token but irreversible.
- `debug` file logs MAY include request headers and response bodies for the model under test. Do not enable `debug` on a session that handles personally identifiable data. The wire dump produces a raw request/response pair; keep it out of issue reports. Operators can route logs to an encrypted directory via `log_dir`.
- OAuth client ID and secret (`packages/core/src/constants.ts`) are part of the public Antigravity CLI client and are not sensitive on their own; they are not a credential pair.
- This README and project tree never contain OAuth tokens, bearer tokens, account file contents, or request/response dumps.

## Troubleshooting

| Symptom | Likely cause | Fix |
| --- | --- | --- |
| OAuth callback hangs | The browser can't reach `localhost:51121` (SSH, remote dev, Docker, WSL2) | `antigravity-auth login --no-browser`, copy the URL into a browser that can reach the host; or `ssh -L 51121:localhost:51121 user@remote` to forward the callback port. |
| Safari shows "Safari can't open the page" | HTTPS-Only mode blocks `http://localhost` callback | Use Chrome or Firefox; or temporarily disable HTTPS-Only mode (Safari → Settings → Privacy) during login. |
| `Address already in use :51121` | Stale OAuth listener from a previous run | `lsof -i :51121` (or `netstat -ano \| findstr :51121` on Windows), kill the listed PID, retry. |
| `Permission 'cloudaicompanion.companions.generateChat' denied` (`rising-fact-p41fc` fallback project) | Antigravity returned no project ID | Enable the `Gemini for Google Cloud API` (`cloudaicompanion.googleapis.com`) on a Google Cloud project and add `projectId` to the account in `antigravity-accounts.json`. Repeat per account in multi-account setups. |
| All accounts show `cooling-down` (quota available) | The threshold filter is excluding the entire pool | Lower `soft_quota_threshold_percent`, or wait for `soft_quota_cache_ttl_minutes` to expire and let the stale-TTL fail-open kick in. Refresh quota via `/antigravity-quota refresh`. |
| Sidebar shows "Awaiting Antigravity state" forever | The state file is missing or unreadable | Confirm `$XDG_STATE_HOME/cortexkit/antigravity-auth/sidebar-state.json` exists and is `0600`; verify the running plugin's user can write to the directory. |
| `Antigravity RPC server is not available` from the TUI | The TUI is polling but no server has bound a port | Confirm the host process actually loaded the plugin (`config.command.antigravity-quota` should appear); the RPC dir (`ANTIGRAVITY_AUTH_RPC_DIR` or its default) must be writable. |
| TUI shows "Awaiting antigravity bundle" / spawn fails | `bun run build:tui` was skipped, or the precompiled tree is missing | Run `bun run --cwd packages/opencode build` (it bundles `tsc` + `esbuild` + `build:tui`); confirm `dist/src/tui-compiled/tui.tsx` exists. |
| `quota_backoff` recovered slowly | `quota_refresh_interval_minutes` is too high | Lower it; or run `/antigravity-quota refresh` to force a refresh. |
| Malformed JSON in `antigravity.json` | Invalid escape / trailing comma | `bun -e 'JSON.parse(require("fs").readFileSync(require("os").homedir()+"/.config/opencode/antigravity.json","utf8"))'`; the schema reference in the file (`$schema: assets/antigravity.schema.json`) drives editor IntelliSense. |
| `Invalid signature in thinking block` errors | `keep_thinking` left on through a Claude version bump | Set `keep_thinking: false`; or clear `signature_cache.disk_ttl_seconds` and restart. The `SKIP_THOUGHT_SIGNATURE` sentinel is injected automatically for known-bad signatures. |
| Pre-`v2` inline quota helper script | Removed in `v2` — the script no longer ships | Use `antigravity-auth quota [--refresh] [--json]`; the npm-installed `antigravity-auth` binary replaces it. |
| Debug log location unknown | `debug` toggled off, or `log_dir` misconfigured | `debug: true` + `log_dir: "/secure/path"`; logs sit under `antigravity-logs/` by default and contain request/response bodies — handle with care. |

When none of the above resolves the issue: stop the host, delete `antigravity-accounts.json` (and optionally `~/.cache/opencode/node_modules/@cortexkit/opencode-antigravity-auth` if a stale npm cache is suspected), and run `opencode auth login` again. Re-add accounts one at a time so a single problem account can be isolated.

## Development workflow

This monorepo uses Bun for contribution commands; consumers install through npm. Keep both in mind when you write a script — npm users run `npx @cortexkit/opencode-antigravity-auth`, contributors run `bun`.

```bash
# at repo root
bun install                                         # sync workspace
bun run build                                       # core -> opencode -> pi
bun run typecheck                                   # tsc across the workspace + scripts tsconfig
bun run test                                        # bun test --isolate across packages
bun run test:e2e                                    # plugin-flow + cli-flow + rpc-tui-flow
bun run test:e2e:models                             # live Antigravity model inventory (gated by CI)
bun run test:e2e:regression                         # cross-model + Gemini CLI regression
bun run --cwd packages/opencode smoke:tui           # bundle + spawn the TUI precompiled tree against the npm package shape
bun run format:check && bun run lint                # Biome format + lint (no auto-fix in CI)
```

The TUI side ships a precompiled JSX tree (`packages/opencode/src/tui-compiled/`) generated by `bun run build:tui` (alias of `bunx tsx packages/opencode/scripts/build-tui.ts`). The output is gitignored; rebuild it as part of every `bun run build`. A smoke-test (`bun run --cwd packages/opencode smoke:tui`) installs the freshly-built artifact as a tarball into a scratch dir and confirms the host can resolve both the `server` and `tui` subpaths. CI runs the smoke-test before TUI-loading tests and before publishing.

`bun run dev` symlinks all packages into a `dist-dev/` tree and watches for source changes. `bun run dev:clean` removes the dev symlinks; use it after switching branches.

Pre-commit hooks (`.lefthook.yml`) run Biome on changed files; CI re-runs the full `bun run format:check && bun run lint` gate.

## Test strategy

The repo splits tests by what they exercise and what they need:

| Scope | Tooling | Network | How to run |
| --- | --- | --- | --- |
| Unit (deterministic) | `bun test --isolate` (`packages/{core,opencode,pi}/src`, `packages/e2e-tests/src`) | No | `bun run test` |
| Black-box e2e (deterministic) | Mock Antigravity server, harness runner (`packages/e2e-tests/`) | No | `bun run test:e2e` |
| Live model inventory | `bun run test:e2e:models` (uses `test-models.ts`) | Yes — gated by CI label | `bun run test:e2e:models` |
| Cross-model regression | `bun run test:e2e:regression` (`script/test-regression.ts`) | Yes | `bun run test:e2e:regression` |
| TUI smoke | `bun run --cwd packages/opencode smoke:tui` (build-tarball install + spawn) | No | `bun run --cwd packages/opencode smoke:tui` |

Deterministic tests run everywhere (including in CI). The live `test:e2e:models` and `test:e2e:regression` flows are gated by label because they hit real Google infrastructure. They live in `packages/opencode/script/` (TypeScript + shell launchers) and use `anthropic-agy` style fixtures kept under `test-fixtures/`.

## Release process

```bash
./scripts/release.sh 1.7.0            # tag-driven release: typecheck -> test -> build -> sync version -> commit + tag + push
./scripts/release.sh 1.7.0 --dry      # dry run: previews version sync without committing/pushing
```

The script enforces semver, refuses to run on a dirty tree, and refuses to run on a non-`main`/`master` branch without confirmation. It runs `bun run typecheck` → `bun run test` → `bun run build` before any mutation. `scripts/version-sync.mjs` is a Node script (no Bun runtime dep) that walks every workspace and writes the new version into each `package.json`. The script tags the release and pushes it; the GitHub workflow is what runs the actual publish.

The `Release` workflow (`.github/workflows/release.yml`) runs on tag push (or `workflow_dispatch`) and:

1. **Test** job — checkout the tag ref → install Bun 1.3 + Node 24 → `bun install --frozen-lockfile` → `bun run typecheck` → `bun run build` → `bun run --cwd packages/opencode smoke:tui` → `bun test` → `bun run test:e2e` → `bun run format:check` → `bun run lint`.
2. **Publish** job — matrixed across the three packages (`@cortexkit/antigravity-auth-core`, `@cortexkit/opencode-antigravity-auth`, `@cortexkit/pi-antigravity-auth`) with `max-parallel: 1` so each package picks up its own OIDC token; `npm Trusted Publishing` (`npm publish --workspace … --access public --provenance`) handles the upload. The job skips a package if its npm version is already published.
3. **GitHub Release** job — `softprops/action-gh-release@v2` creates the GitHub release with `generate_release_notes: true`.

CI (`.github/workflows/ci.yml`) reuses the same typecheck → build → smoke → test → e2e → format → lint chain on every push to `main` and on every pull request. The `issue-triage.yml` workflow handles GitHub-issue routing.

## Architecture and structure links

- [ARCHITECTURE.md](ARCHITECTURE.md) — system-of-record architecture doc across all three packages.
- [STRUCTURE.md](STRUCTURE.md) — file-system map of the monorepo.
- [packages/opencode/ARCHITECTURE.md](packages/opencode/ARCHITECTURE.md) — OpenCode plugin architecture in detail.
- [packages/opencode/STRUCTURE.md](packages/opencode/STRUCTURE.md) — OpenCode package file-system map.
- [packages/opencode/CHANGELOG.md](packages/opencode/CHANGELOG.md) — version history (current is `2.0.0`; previous major was `1.6.0`).
- [packages/opencode/docs/CONFIGURATION.md](packages/opencode/docs/CONFIGURATION.md) — detailed option walkthrough.
- [packages/opencode/docs/MULTI-ACCOUNT.md](packages/opencode/docs/MULTI-ACCOUNT.md) — load balancing, dual quota pools, account storage.
- [packages/opencode/docs/MODEL-VARIANTS.md](packages/opencode/docs/MODEL-VARIANTS.md) — variants, thinking budgets, model selection.
- [packages/opencode/docs/TROUBLESHOOTING.md](packages/opencode/docs/TROUBLESHOOTING.md) — long-form troubleshooting with platform-specific commands.
- [packages/opencode/docs/ANTIGRAVITY_API_SPEC.md](packages/opencode/docs/ANTIGRAVITY_API_SPEC.md) — Antigravity API reference.
- [packages/opencode/assets/antigravity.schema.json](packages/opencode/assets/antigravity.schema.json) — generated JSON Schema for IDE validation.

## License

MIT. See [LICENSE](LICENSE).
