Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,44 @@ For details on load balancing, dual quota pools, and account storage, see [docs/

---

## Proxy Configuration

Use HTTP/HTTPS proxies for all API traffic (OAuth, token refresh, API calls). Useful for corporate environments or anti-detection setups.

### Quick Start

```bash
# Login with proxy - automatically saves to account
ANTIGRAVITY_LOGIN_PROXY=http://proxy.example.com:8080 opencode auth login

# All future API calls from this account use the proxy
opencode run "Hello" --model=google/antigravity-claude-sonnet-4-5
```

### How It Works

1. **OAuth Login**: Set `ANTIGRAVITY_LOGIN_PROXY` during `opencode auth login`
2. **Auto-Save**: Proxy URL is saved to `~/.config/opencode/antigravity-accounts.json`
3. **All Traffic**: All API calls (token refresh, project discovery, API requests) use the proxy

### Supported Formats

- `http://proxy.example.com:8080` - Unauthenticated
- `http://user:[email protected]:8080` - Authenticated
- `https://proxy.example.com:443` - TLS proxy

**Note**: SOCKS5 proxies are NOT supported. Use HTTP/HTTPS only.

### Security Warning

⚠️ Proxy credentials are stored in **plaintext** in `antigravity-accounts.json` (same as OAuth tokens). Use filesystem permissions to protect this file.

### Advanced: Manual Configuration

See [docs/PROXY.md](docs/PROXY.md) for manual JSON editing and troubleshooting.

---

## Troubleshoot

> **Quick Reset**: Most issues can be resolved by deleting `~/.config/opencode/antigravity-accounts.json` and running `opencode auth login` again.
Expand Down
140 changes: 140 additions & 0 deletions docs/PROXY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# Proxy Configuration Example

## OAuth Login Proxy (Recommended)

**New in v1.3.4**: Use the `ANTIGRAVITY_LOGIN_PROXY` environment variable to configure proxy during account login. The proxy URL will be saved to the account automatically.

```bash
# Login with proxy
ANTIGRAVITY_LOGIN_PROXY=http://proxy.example.com:8080 opencode auth login

# Login with authenticated proxy
ANTIGRAVITY_LOGIN_PROXY=http://user:[email protected]:8080 opencode auth login
```

The proxy URL is saved to `~/.config/opencode/antigravity-accounts.json` and used for:
- All OAuth token refreshes
- Project discovery API calls
- Gemini/Claude API requests
- Google Search tool requests
- Quota check requests

**Benefits:**
- No manual JSON editing required
- Proxy is automatically associated with the account
- All future API calls from this account use the configured proxy

## Manual Proxy Configuration (Alternative)

Alternatively, you can manually edit your `~/.config/opencode/antigravity-accounts.json` file and add `proxyUrl` fields to each account:

```json
{
"version": 3,
"accounts": [
{
"email": "[email protected]",
"refreshToken": "1//0abc...",
"projectId": "my-project-1",
"proxyUrl": "http://user1:[email protected]:8080",
"addedAt": 1704067200000,
"lastUsed": 1704153600000
},
{
"email": "[email protected]",
"refreshToken": "1//0def...",
"projectId": "my-project-2",
"proxyUrl": "http://user2:[email protected]:8080",
"addedAt": 1704067300000,
"lastUsed": 1704153700000
},
{
"email": "[email protected]",
"refreshToken": "1//0ghi...",
"projectId": "my-project-3",
"proxyUrl": "https://proxy3.example.com:443",
"addedAt": 1704067400000,
"lastUsed": 1704153800000
}
],
"activeIndex": 0,
"activeIndexByFamily": {
"claude": 0,
"gemini": 1
}
}
```

## Supported Proxy Formats

- **HTTP**: `http://[user:pass@]host:port`
- **HTTPS**: `https://[user:pass@]host:port`

**Note**: SOCKS5 proxies are NOT currently supported. Use HTTP/HTTPS proxies only.

## Anti-Detection Features

1. **Hard Fail**: If proxy fails, request fails immediately - NO direct fallback
2. **All Traffic**: Token refresh, project discovery, and API calls all use same proxy
3. **Per-Account Isolation**: Each account uses its own proxy → unique IP per account
4. **Connection Pooling**: Proxy connections are cached and reused for performance

## Important Notes

- **Credentials**: Proxy passwords stored in plaintext (same security level as OAuth tokens)
- **Backward Compatible**: Accounts without `proxyUrl` work unchanged (direct connection)
- **Restart Required**: Changes to `antigravity-accounts.json` require OpenCode restart
- **Error Handling**: Failed proxy connections mark account "cooling down" for 30 seconds

## Testing Your Proxies

Before adding proxies to all accounts, test one account first:

```bash
# Test proxy during login
ANTIGRAVITY_LOGIN_PROXY=http://localhost:8080 opencode auth login

# Watch your proxy logs - you should see:
# - POST https://oauth2.googleapis.com/token
# - GET https://www.googleapis.com/oauth2/v1/userinfo
# - POST to Antigravity loadCodeAssist endpoints

# Make a test request
opencode run "Hello" --model=google/claude-sonnet-4-5

# Proxy logs should show API traffic
```

## Troubleshooting

### Proxy Connection Failed

```
Error: Failed to create proxy agent for http://proxy:8080: connect ECONNREFUSED
```

**Solutions:**
- Check proxy URL format: `http://host:port` (not `https://` unless TLS-enabled proxy)
- Test proxy with curl: `curl -x http://proxy:8080 https://google.com`
- Ensure proxy is running and accessible
- Check firewall rules

### Invalid Proxy URL Format

```
Error: Invalid proxy URL format: http://***:***@:invalid
```

**Solutions:**
- Ensure URL format is correct: `http://user:pass@host:port`
- Host must be valid hostname or IP address
- Port must be numeric

### Account Cooldown After Proxy Failures

If proxy fails 5 times consecutively, the account enters a 30-second cooldown to prevent cascading failures.

**Solutions:**
- Fix proxy configuration
- Wait 30 seconds for cooldown to expire
- Check proxy logs for error details
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"@opencode-ai/plugin": "^0.15.30",
"@openauthjs/openauth": "^0.4.3",
"proper-lockfile": "^4.1.2",
"undici": "^6.23.0",
"xdg-basedir": "^5.1.0",
"zod": "^4.0.0"
}
Expand Down
17 changes: 13 additions & 4 deletions src/antigravity/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from "../constants";
import { createLogger } from "../plugin/logger";
import { calculateTokenExpiry } from "../plugin/auth";
import { fetchWithProxy } from "../plugin/proxy";

const log = createLogger("oauth");

Expand Down Expand Up @@ -119,17 +120,22 @@ async function fetchWithTimeout(
url: string,
options: RequestInit,
timeoutMs = FETCH_TIMEOUT_MS,
proxyUrl?: string,
): Promise<Response> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
if (proxyUrl) {
return await fetchWithProxy(url, { ...options, signal: controller.signal }, proxyUrl);
}
return await fetch(url, { ...options, signal: controller.signal });
} finally {
clearTimeout(timeout);
}
}

async function fetchProjectID(accessToken: string): Promise<string> {
const proxyUrl = process.env.ANTIGRAVITY_LOGIN_PROXY;
const errors: string[] = [];
const loadHeaders: Record<string, string> = {
Authorization: `Bearer ${accessToken}`,
Expand All @@ -156,7 +162,7 @@ async function fetchProjectID(accessToken: string): Promise<string> {
pluginType: "GEMINI",
},
}),
});
}, undefined, proxyUrl);

if (!response.ok) {
const message = await response.text().catch(() => "");
Expand Down Expand Up @@ -203,11 +209,12 @@ export async function exchangeAntigravity(
code: string,
state: string,
): Promise<AntigravityTokenExchangeResult> {
const proxyUrl = process.env.ANTIGRAVITY_LOGIN_PROXY;
try {
const { verifier, projectId } = decodeState(state);

const startTime = Date.now();
const tokenResponse = await fetch("https://oauth2.googleapis.com/token", {
const tokenResponse = await fetchWithTimeout("https://oauth2.googleapis.com/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
Expand All @@ -224,7 +231,7 @@ export async function exchangeAntigravity(
redirect_uri: ANTIGRAVITY_REDIRECT_URI,
code_verifier: verifier,
}),
});
}, undefined, proxyUrl);

if (!tokenResponse.ok) {
const errorText = await tokenResponse.text();
Expand All @@ -233,7 +240,7 @@ export async function exchangeAntigravity(

const tokenPayload = (await tokenResponse.json()) as AntigravityTokenResponse;

const userInfoResponse = await fetch(
const userInfoResponse = await fetchWithTimeout(
"https://www.googleapis.com/oauth2/v1/userinfo?alt=json",
{
headers: {
Expand All @@ -242,6 +249,8 @@ export async function exchangeAntigravity(
"X-Goog-Api-Client": GEMINI_CLI_HEADERS["X-Goog-Api-Client"],
},
},
undefined,
proxyUrl
);

const userInfo = userInfoResponse.ok
Expand Down
Loading