Authentication Chain — OAuth2, Keychain, and Token Lifecycle
Complete authentication chain analysis: TrySilentAuth flow, credential management, keychain format, scopes, passive refresh via Heartbeat, and installation IDs
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
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:
{
"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.
// 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) |
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
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
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
{
"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:
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
curl -s "https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=[REDACTED]" | python3 -m json.tool
Response:
{
"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:
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:
TrySilentAuth()is called as a side effect- If token is expired, it is transparently refreshed
- The refreshed token is written back to the keychain
- ALL other components (Master, IDE, SDK) benefit from the fresh token
Recommended Cron
# 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
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:
- Generates PKCE parameters (code_verifier, code_challenge)
- Prints the authorization URL to stdout
- Opens the URL in the system browser
- Waits for the user to paste the authorization code
- Exchanges the code for tokens
- 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.