Skip to content
This repository was archived by the owner on Mar 30, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all 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
2,695 changes: 1,253 additions & 1,442 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@
"dependencies": {
"@opencode-ai/plugin": "^0.15.30",
"@openauthjs/openauth": "^0.4.3",
"fetch-socks": "^1.3.0",
"proper-lockfile": "^4.1.2",
"undici": "^7.0.0",
"xdg-basedir": "^5.1.0",
"zod": "^4.0.0"
}
Expand Down
4 changes: 3 additions & 1 deletion script/test-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const MODELS: ModelTest[] = [

const TEST_PROMPT = "Reply with exactly one word: WORKING";
const DEFAULT_TIMEOUT_MS = 120_000;
const OPENCODE_BIN = process.platform === "win32" ? "opencode.cmd" : "opencode";

interface TestResult {
success: boolean;
Expand All @@ -44,8 +45,9 @@ async function testModel(model: string, timeoutMs: number): Promise<TestResult>
const start = Date.now();

return new Promise((resolve) => {
const proc = spawn("opencode", ["run", TEST_PROMPT, "--model", model], {
const proc = spawn(OPENCODE_BIN, ["run", TEST_PROMPT, "--model", model], {
stdio: ["ignore", "pipe", "pipe"],
shell: process.platform === "win32",
});

let stdout = "";
Expand Down
7 changes: 5 additions & 2 deletions script/test-regression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const GEMINI_FLASH = "google/antigravity-gemini-3-flash";
const GEMINI_FLASH_CLI_QUOTA = "google/gemini-2.5-flash";
const CLAUDE_SONNET = "google/antigravity-claude-sonnet-4-5-thinking-low";
const CLAUDE_OPUS = "google/antigravity-claude-opus-4-5-thinking-low";
const OPENCODE_BIN = process.platform === "win32" ? "opencode.cmd" : "opencode";

const SANITY_TESTS: MultiTurnTest[] = [
{
Expand Down Expand Up @@ -291,8 +292,9 @@ async function runTurn(
? ["run", prompt, "--session", sessionId, "--model", model]
: ["run", prompt, "--model", model, "--title", sessionTitle];

const proc = spawn("opencode", args, {
const proc = spawn(OPENCODE_BIN, args, {
stdio: ["ignore", "pipe", "pipe"],
shell: process.platform === "win32",
cwd: process.cwd(),
});

Expand Down Expand Up @@ -345,8 +347,9 @@ async function runTurn(

async function deleteSession(sessionId: string): Promise<void> {
return new Promise((resolve) => {
const proc = spawn("opencode", ["session", "delete", sessionId, "--force"], {
const proc = spawn(OPENCODE_BIN, ["session", "delete", sessionId, "--force"], {
stdio: ["ignore", "pipe", "pipe"],
shell: process.platform === "win32",
timeout: 10000,
cwd: process.cwd(),
});
Expand Down
107 changes: 107 additions & 0 deletions src/antigravity/oauth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { beforeEach, describe, expect, it, vi } from "vitest"

const { fetchWithProxyMock } = vi.hoisted(() => ({
fetchWithProxyMock: vi.fn(),
}))

vi.mock("../plugin/proxy", () => ({
fetchWithProxy: fetchWithProxyMock,
}))

import { exchangeAntigravity } from "./oauth"

function makeState(verifier: string, projectId = ""): string {
return Buffer.from(JSON.stringify({ verifier, projectId }), "utf8").toString("base64url")
}

describe("exchangeAntigravity proxy routing", () => {
beforeEach(() => {
vi.clearAllMocks()
})

it("routes token and userinfo requests through account-scoped proxy args", async () => {
fetchWithProxyMock
.mockResolvedValueOnce(
new Response(
JSON.stringify({
access_token: "access-1",
expires_in: 3600,
refresh_token: "refresh-1",
}),
{ status: 200 },
),
)
.mockResolvedValueOnce(
new Response(
JSON.stringify({ email: "user@example.com" }),
{ status: 200 },
),
)

const proxies = [{ url: "socks5://127.0.0.1:1080" }]
const accountIndex = 4

const result = await exchangeAntigravity(
"auth-code",
makeState("pkce-verifier", "project-1"),
proxies,
accountIndex,
)

expect(result.type).toBe("success")
expect(fetchWithProxyMock).toHaveBeenCalledTimes(2)

const firstCall = fetchWithProxyMock.mock.calls[0] ?? []
const secondCall = fetchWithProxyMock.mock.calls[1] ?? []

expect(firstCall[0]).toBe("https://oauth2.googleapis.com/token")
expect(secondCall[0]).toBe("https://www.googleapis.com/oauth2/v1/userinfo?alt=json")

expect(firstCall[2]).toEqual(proxies)
expect(firstCall[3]).toBe(accountIndex)
expect(secondCall[2]).toEqual(proxies)
expect(secondCall[3]).toBe(accountIndex)
})

it("routes project discovery request through account-scoped proxy args when projectId is missing", async () => {
fetchWithProxyMock
.mockResolvedValueOnce(
new Response(
JSON.stringify({
access_token: "access-2",
expires_in: 3600,
refresh_token: "refresh-2",
}),
{ status: 200 },
),
)
.mockResolvedValueOnce(new Response("{}", { status: 500 }))
.mockResolvedValueOnce(
new Response(
JSON.stringify({
cloudaicompanionProject: "resolved-project",
}),
{ status: 200 },
),
)

const proxies = [{ url: "http://127.0.0.1:8080" }]
const accountIndex = 9

const result = await exchangeAntigravity(
"auth-code",
makeState("pkce-verifier"),
proxies,
accountIndex,
)

expect(result.type).toBe("success")
expect(fetchWithProxyMock).toHaveBeenCalledTimes(3)

const projectCall = fetchWithProxyMock.mock.calls[2] ?? []
expect(typeof projectCall[0]).toBe("string")
expect(projectCall[0]).toContain("/v1internal:loadCodeAssist")
expect(projectCall[2]).toEqual(proxies)
expect(projectCall[3]).toBe(accountIndex)
})
})
57 changes: 36 additions & 21 deletions src/antigravity/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
} from "../constants";
import { createLogger } from "../plugin/logger";
import { calculateTokenExpiry } from "../plugin/auth";
import { fetchWithProxy } from "../plugin/proxy";
import type { ProxyConfig } from "../plugin/storage";

const log = createLogger("oauth");

Expand Down Expand Up @@ -119,17 +121,19 @@ async function fetchWithTimeout(
url: string,
options: RequestInit,
timeoutMs = FETCH_TIMEOUT_MS,
proxies?: ProxyConfig[],
accountIndex?: number,
): Promise<Response> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
return await fetch(url, { ...options, signal: controller.signal });
return await fetchWithProxy(url, { ...options, signal: controller.signal }, proxies, accountIndex);
} finally {
clearTimeout(timeout);
}
}

async function fetchProjectID(accessToken: string): Promise<string> {
async function fetchProjectID(accessToken: string, proxies?: ProxyConfig[], accountIndex?: number): Promise<string> {
const errors: string[] = [];
const loadHeaders: Record<string, string> = {
Authorization: `Bearer ${accessToken}`,
Expand All @@ -155,7 +159,7 @@ async function fetchProjectID(accessToken: string): Promise<string> {
pluginType: "GEMINI",
},
}),
});
}, FETCH_TIMEOUT_MS, proxies, accountIndex);

if (!response.ok) {
const message = await response.text().catch(() => "");
Expand Down Expand Up @@ -201,28 +205,36 @@ async function fetchProjectID(accessToken: string): Promise<string> {
export async function exchangeAntigravity(
code: string,
state: string,
proxies?: ProxyConfig[],
accountIndex?: number,
): Promise<AntigravityTokenExchangeResult> {
try {
const { verifier, projectId } = decodeState(state);

const startTime = Date.now();
const tokenResponse = await fetch("https://oauth2.googleapis.com/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate, br",
"User-Agent": GEMINI_CLI_HEADERS["User-Agent"],
const tokenResponse = await fetchWithTimeout(
"https://oauth2.googleapis.com/token",
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate, br",
"User-Agent": GEMINI_CLI_HEADERS["User-Agent"],
},
body: new URLSearchParams({
client_id: ANTIGRAVITY_CLIENT_ID,
client_secret: ANTIGRAVITY_CLIENT_SECRET,
code,
grant_type: "authorization_code",
redirect_uri: ANTIGRAVITY_REDIRECT_URI,
code_verifier: verifier,
}),
},
body: new URLSearchParams({
client_id: ANTIGRAVITY_CLIENT_ID,
client_secret: ANTIGRAVITY_CLIENT_SECRET,
code,
grant_type: "authorization_code",
redirect_uri: ANTIGRAVITY_REDIRECT_URI,
code_verifier: verifier,
}),
});
FETCH_TIMEOUT_MS,
proxies,
accountIndex,
);

if (!tokenResponse.ok) {
const errorText = await tokenResponse.text();
Expand All @@ -231,14 +243,17 @@ 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: {
Authorization: `Bearer ${tokenPayload.access_token}`,
"User-Agent": GEMINI_CLI_HEADERS["User-Agent"],
},
},
FETCH_TIMEOUT_MS,
proxies,
accountIndex,
);

const userInfo = userInfoResponse.ok
Expand All @@ -252,7 +267,7 @@ export async function exchangeAntigravity(

let effectiveProjectId = projectId;
if (!effectiveProjectId) {
effectiveProjectId = await fetchProjectID(tokenPayload.access_token);
effectiveProjectId = await fetchProjectID(tokenPayload.access_token, proxies, accountIndex);
}

const storedRefresh = `${refreshToken}|${effectiveProjectId || ""}`;
Expand Down
Loading