---
name: sdk-test-auth-chain
type: analysis
title: "Authentication Chain — OAuth2, Keychain, and Token Lifecycle"
description: "Complete authentication chain analysis: TrySilentAuth flow, credential management, keychain format, scopes, passive refresh via Heartbeat, and installation IDs"
tags: [auth, oauth2, keychain, token, pkce, refresh, security, antigravity]
timestamp: 2026-07-22
---

# Authentication Chain — OAuth2, Keychain, and Token Lifecycle

## Overview

The JETSKY ecosystem uses a **unified OAuth2 authentication layer** shared across all components (Master, IDE, CLI, SDK). Tokens are stored in the macOS Keychain as the primary store, with a file-based fallback at `~/.gemini/`. The authentication chain was reverse-engineered from the `agy` Go binary via Ghidra decompilation (3,305 functions).

**All credentials on this page are [REDACTED]. No real secrets are stored in the wiki.**

## Complete Authentication Flow

```mermaid
flowchart TD
    A["API Call Needed<br/>(any gRPC or model request)"] --> B["TrySilentAuth()"]
    
    B --> C{"Memory cache<br/>(singleton AuthProvider)"}
    C -->|"Cache hit + valid TTL"| F["Return Bearer<br/>access_token"]
    C -->|"Cache miss or expired"| D{"ChainedAuth()"}
    
    D --> E["keyringAuth()<br/>[Primary Strategy]"]
    E --> E1{"macOS Keychain<br/>service: gemini<br/>account: antigravity"}
    E1 -->|"Found + valid TTL"| F
    E1 -->|"Found + expired"| G["refreshAndSaveToken()"]
    E1 -->|"Not found (1s timeout)"| H["fileAuth()<br/>[Fallback Strategy]"]
    
    H --> H1{"~/.gemini/<br/>token file"}
    H1 -->|"Found + valid TTL"| F
    H1 -->|"Found + expired"| G
    H1 -->|"Not found"| I["interactiveAuth()<br/>[Last Resort]"]
    
    I --> I1["PKCE Browser Flow<br/>accounts.google.com/o/oauth2/v2/auth"]
    I1 --> I2["User authorizes in browser"]
    I2 --> G
    
    G --> G1["POST oauth2.googleapis.com/token"]
    G1 --> G2["applyAuthResult()"]
    G2 --> G3["Write to Keychain"]
    G2 --> G4["Write to File Fallback"]
    G2 --> G5["Update Memory Cache"]
    G3 --> F
    G4 --> F
    G5 --> F
```

## Decompiled Auth Functions

The following functions were decompiled from the `agy` binary:

| Function | Package Path | Purpose |
|----------|-------------|---------|
| `TrySilentAuth` | `auth.(*AuthProvider)` | Entry point. Checks memory cache, delegates to ChainedAuth if expired |
| `ChainedAuth` | `auth.(*AuthProvider)` | Chains strategies: keyring → file → interactive |
| `keyringAuth` | `auth.(*AuthProvider)` | Reads from macOS Keychain via `go-keyring` library |
| `fileAuth` | `auth.(*AuthProvider)` | Reads from `~/.gemini/` file fallback |
| `interactiveAuth` | `auth.(*AuthProvider)` | Launches PKCE browser flow as last resort |
| `refreshAndSaveToken` | `auth.(*AuthProvider)` | POSTs to Google OAuth2 endpoint, calls applyAuthResult |
| `applyAuthResult` | `auth.(*AuthProvider)` | Writes refreshed credentials to all three storage layers |
| `GetGrantedScopes` | `auth.(*AuthProvider)` | Returns scopes from original PKCE authorization |
| `generateState` | `authclient` | Generates PKCE state parameter |
| `openBrowser` | `authclient` | Opens system browser for PKCE flow |
| `performTerminalAuthFlow` | `authclient` | Full PKCE flow for terminal/CLI context |
| `validateLoginAndUpdateStatus` | `authclient` | Validates login state and updates internal status |
| `LoginWithBrowser` | `authclient` | Initiates browser-based login |
| `GetAuthStatus` | `authclient` | Returns current authentication status |

## Credential Management

### OAuth2 Client IDs

Three distinct OAuth2 Client ID/Secret pairs exist in the binary:

#### Consumer (ACTIVE)

| Field | Value |
|-------|-------|
| Client ID | [REDACTED] |
| Client Secret | [REDACTED] |
| Usage | Google One / Consumer OAuth2 |
| Status | Active -- used by agy CLI + IDE + Master |
| Scopes | Full scope set (see below) |

#### Enterprise

| Field | Value |
|-------|-------|
| Client ID | [REDACTED] |
| Client Secret | [REDACTED] |
| Usage | GCP / Code Assist enterprise flows |
| Status | Available in binary, not active by default |

#### Legacy (DISCONTINUED)

| Field | Value |
|-------|-------|
| Client ID | [REDACTED] |
| Client Secret | [REDACTED] |
| Usage | `opencode-gemini-auth` (legacy Antigravity) |
| Status | Discontinued -- credential file deleted |

### OAuth2 Scopes

```
openid
email
profile
https://www.googleapis.com/auth/userinfo.email
https://www.googleapis.com/auth/userinfo.profile
https://www.googleapis.com/auth/cloud-platform
https://www.googleapis.com/auth/cclog
https://www.googleapis.com/auth/experimentsandconfigs
```

| Scope | Purpose | Sensitivity |
|-------|---------|-------------|
| `openid` | OpenID Connect | Low |
| `email` | User email access | Medium |
| `profile` | User profile access | Medium |
| `userinfo.email` | Email via userinfo endpoint | Medium |
| `userinfo.profile` | Profile via userinfo endpoint | Medium |
| `cloud-platform` | Full GCP API access | **High** |
| `cclog` | Corporate logging | Low |
| `experimentsandconfigs` | Feature flags / A/B experiments | Low |

The `cloud-platform` scope is the most significant -- it grants full Google Cloud Platform API access. This is necessary for Vertex AI backend routing and GCP project validation.

## Keychain Storage Format

### Structure

```
Service:  "gemini"
Account:  "antigravity"
Format:   go-keyring-base64:<base64(JSON)>
Auth Method: "consumer"
```

### Decoded JSON Schema

The base64-decoded value is a JSON object:

```json
{
  "access_token": "[REDACTED]",
  "refresh_token": "[REDACTED]",
  "token_type": "Bearer",
  "expiry": "2026-07-22T15:30:00Z",
  "client_id": "[REDACTED]",
  "client_secret": "[REDACTED]",
  "granted_scopes": [
    "openid",
    "email",
    "profile",
    "https://www.googleapis.com/auth/userinfo.email",
    "https://www.googleapis.com/auth/userinfo.profile",
    "https://www.googleapis.com/auth/cloud-platform",
    "https://www.googleapis.com/auth/cclog",
    "https://www.googleapis.com/auth/experimentsandconfigs"
  ]
}
```

| Field | Type | Description |
|-------|------|-------------|
| `access_token` | string | Current OAuth2 Bearer token (JWT, opaque) |
| `refresh_token` | string | Offline refresh token (persistent, long-lived) |
| `token_type` | string | Always "Bearer" |
| `expiry` | string | RFC 3339 timestamp of access_token expiry |
| `client_id` | string | OAuth2 client ID that obtained this token |
| `client_secret` | string | OAuth2 client secret |
| `granted_scopes` | string[] | Scopes granted during PKCE authorization |

### Keychain Read Timeout

The keychain read has a **1-second timeout**. If the macOS Keychain is locked (e.g., screen locked, keychain locked after sleep), the auth chain falls through to the file fallback. This prevents the auth chain from hanging indefinitely.

```go
// Decompiled pattern (simplified)
func keyringAuth() (*Token, error) {
    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    defer cancel()
    
    value, err := keyring.GetWithContext(ctx, "gemini", "antigravity")
    if err != nil {
        return nil, err  // Falls through to fileAuth()
    }
    return parseKeyringValue(value)
}
```

## Token Lifecycle

### TTL Characteristics

| Property | Value |
|----------|-------|
| Type | OAuth2 Bearer Token |
| TTL | ~3600 seconds (1 hour) |
| Format | JWT (opaque to the client) |
| Refresh | Via `refresh_token` (offline, persistent) |
| Storage | Keychain (primary) + `~/.gemini/` (fallback) + Memory (cache) |

```mermaid
timeline
    title Token Lifecycle Timeline
    section Token A
        PKCE Auth : access_token A issued
        Active Use : 0-3600s
    section Token B
        Auto Refresh : access_token B issued
        Active Use : 3600-7200s
    section Token C
        Auto Refresh : access_token C issued
        Active Use : 7200-10800s
```

### Triple Storage Architecture

```mermaid
graph TB
    subgraph "AuthProvider Singleton"
        MC["Memory Cache<br/>access_token + expiry<br/>Fastest, per-process"]
    end

    subgraph "Persistent Storage"
        KC["macOS Keychain<br/>Service: gemini<br/>Account: antigravity<br/>Primary, 1s timeout"]
        FF["File Fallback<br/>~/.gemini/<br/>Secondary, no timeout"]
    end

    MC <-->|"read/write"| KC
    MC <-->|"read/write"| FF
    KC <-.->|"synced via<br/>applyAuthResult"| FF

    style MC fill:#2a4a6b
    style KC fill:#4a6b2a
    style FF fill:#6b4a2a
```

### Token Refresh Mechanics

#### HTTP Request

```http
POST https://oauth2.googleapis.com/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&client_id=[REDACTED]
&client_secret=[REDACTED]
&refresh_token=[REDACTED]
```

#### Response

```json
{
  "access_token": "[REDACTED]",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "openid email profile ..."
}
```

Note: The refresh response does NOT include a new `refresh_token`. The original refresh token persists indefinitely (until revoked).

#### applyAuthResult

After a successful refresh, `applyAuthResult()` writes to all three storage layers:

```mermaid
sequenceDiagram
    participant Refresh as refreshAndSaveToken
    participant Apply as applyAuthResult
    participant KC as macOS Keychain
    participant FF as File Fallback
    participant MC as Memory Cache

    Refresh->>Apply: new access_token + expiry

    par Write to all stores
        Apply->>KC: keyring.Set("gemini", "antigravity", "go-keyring-base64:" + base64(json))
        Apply->>FF: Write JSON to ~/.gemini/ token file
        Apply->>MC: Update singleton AuthProvider
    end

    Note over KC,FF,MC: All three stores are now synchronized
```

### When Does Refresh Happen?

| Trigger | Mechanism | Automatic? |
|---------|-----------|------------|
| Any `agy` CLI command | `TrySilentAuth()` → TTL check | Yes |
| `agy --print <anything>` | Same auth chain | Yes |
| Heartbeat cron job | Invokes `agy --print` | Yes (via cron) |
| SDK API call | SDK invokes auth before gRPC | Yes |
| First boot after sleep | Token may be expired, refreshes on first use | Yes |

**There is no background daemon or periodic timer in the `agy` binary**. Refresh only happens when the binary is invoked. This is why the Heartbeat cron is important.

### Token Validation

```bash
curl -s "https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=[REDACTED]" | python3 -m json.tool
```

Response:
```json
{
  "azp": "[REDACTED]",
  "aud": "[REDACTED]",
  "scope": "openid email profile ...",
  "expires_in": 2847,
  "email": "[REDACTED]",
  "email_verified": "true"
}
```

## Installation IDs

Each JETSKY component has an installation ID used for telemetry and client identification.

| Component | Installation ID | Shared With |
|-----------|-----------------|-------------|
| Antigravity Master | [REDACTED] | IDE |
| Antigravity IDE | [REDACTED] | Master |
| Antigravity CLI (agy) | [REDACTED] | None |

**Key observation**: Master and IDE share the same installation ID. This means they appear as a single client to Google's backend. One PKCE authorization covers both applications. The CLI has a separate installation ID for independent telemetry tracking.

### Installation ID in Telemetry

The installation ID appears in the `ProductEvent` protobuf:

```protobuf
message ProductEvent {
  string event_name = 1;
  string api_key = 2;
  string installation_id = 3;
  string ide_name = 4;
  string os = 5;
  string codeium_version = 6;
  int64 timestamp = 7;
  map<string, string> properties = 8;
}
```

## Passive Refresh via Heartbeat

The Heartbeat Protocol exploits the auth chain to keep tokens fresh across the ecosystem.

### Mechanism

Running `agy --print "<any prompt>"` triggers:
1. `TrySilentAuth()` is called as a side effect
2. If token is expired, it is transparently refreshed
3. The refreshed token is written back to the keychain
4. ALL other components (Master, IDE, SDK) benefit from the fresh token

### Recommended Cron

```bash
# Every 45 minutes — well within the 1-hour TTL
*/45 * * * * /Users/alefita/.local/bin/agy --print "[REDACTED]" > /dev/null 2>&1
```

See [[sdk-test-deepseek-heartbeat]] for the complete Heartbeat Protocol including Proto-Unicode sequences.

### Timing Analysis

| Scenario | Latency | Side Effect |
|----------|---------|-------------|
| Token valid (TTL > 0) | ~100ms | No refresh needed |
| Token expired (TTL <= 0) | ~2-3s | Token refreshed and written to keychain |
| Keychain locked | ~1s timeout + file fallback | Falls through to file auth |
| Network down | Variable | Refresh fails, cached token used |

## PKCE Flow (Interactive Auth)

When both keyring and file auth fail, the system launches a PKCE (Proof Key for Code Exchange) browser flow.

### Flow Sequence

```mermaid
sequenceDiagram
    participant CLI as agy CLI
    participant Browser as System Browser
    participant Google as Google OAuth2
    participant KC as macOS Keychain

    CLI->>CLI: generateState() → random state param
    CLI->>CLI: Generate code_verifier + code_challenge (S256)
    CLI->>Browser: openBrowser(PKCE URL)
    Note right of Browser: URL: accounts.google.com/o/oauth2/v2/auth<br/>?response_type=code<br/>&client_id=[REDACTED]<br/>&redirect_uri=urn:ietf:wg:oauth:2.0:oob<br/>&scope=openid+email+profile+...<br/>&state=[random]<br/>&code_challenge=[S256]<br/>&code_challenge_method=S256

    Browser->>Google: User authorizes
    Google-->>Browser: Authorization code
    Browser-->>CLI: Display authorization code (OOB)
    CLI->>Google: Exchange code + code_verifier for tokens
    Google-->>CLI: access_token + refresh_token
    CLI->>KC: applyAuthResult() → write to all stores
```

### Redirect URI

The PKCE flow uses `urn:ietf:wg:oauth:2.0:oob` (out-of-band) as the redirect URI. This means the authorization code is displayed directly in the browser for the user to copy, rather than being redirected to a local server. This is appropriate for CLI tools that cannot host a local HTTP server.

### Terminal Auth Flow

The `performTerminalAuthFlow` function handles the interactive PKCE flow specifically for terminal contexts:

1. Generates PKCE parameters (code_verifier, code_challenge)
2. Prints the authorization URL to stdout
3. Opens the URL in the system browser
4. Waits for the user to paste the authorization code
5. Exchanges the code for tokens
6. Calls `applyAuthResult()` to persist

## Security Considerations

### Fail-Closed Design

The auth chain follows a fail-closed design:
- Keychain read timeout: 1 second (prevents hanging)
- Token refresh failure: Returns cached (possibly stale) token
- All strategies exhausted: Raises error (does NOT silently proceed)

### Credential Isolation

- Consumer credentials are used by default for personal accounts
- Enterprise credentials are available but not active by default
- Legacy credentials are discontinued
- Each credential pair has its own scope set

### Keychain Security

The macOS Keychain provides:
- Encryption at rest (hardware-backed on Apple Silicon)
- Access control (requires user authorization)
- Per-application access policies
- Audit logging

The 1-second timeout on keychain reads is a deliberate trade-off: it prevents the auth chain from blocking when the keychain is locked, at the cost of occasionally falling through to the less secure file fallback.

### Refresh Token Lifetime

The `refresh_token` is an offline token with no explicit expiry. It remains valid until:
- The user revokes access
- The OAuth2 client is deactivated
- Google invalidates it (rare)

This means a single PKCE authorization can sustain token refreshes indefinitely, as long as the refresh token is preserved in the keychain.

## Cross-References

- [[sdk-test-architecture]] — JETSKY ecosystem architecture and binary inventory
- [[sdk-test-grpc-protocol]] — How auth tokens are used in gRPC headers
- [[sdk-test-deepseek-heartbeat]] — Heartbeat Protocol for passive token refresh
- [[sdk-test-reverse-engineering]] — Decompiled auth functions (full list)
- [[sdk-test-localharness]] — How the harness uses auth for Gemini API calls
- [[sdk-test-protobuf]] — ClientInfo and auth-related protobuf messages

---

*Source: Decompiled from `agy` binary (ARM64, ~99MB) via Ghidra MCP. Functions: `authclient.*` (12 functions), `auth.*` (AuthProvider methods). Keychain format verified via `security find-generic-password -s gemini -a antigravity`.*
