feat(antigravity): initial steps for Antigravity anti-ban alignment

Cherry-picked from decolua/9router#141 (author: LinearSakana / zx <me@char.moe>)

- Implement client identity spoofing with numeric enums (ideType: 9, pluginType: 2)
- Add runtime platform detection for User-Agent and metadata
- Implement per-connection session ID caching (binary-compatible format)
- Add ANTIGRAVITY_HEADERS (X-Client-Name, X-Client-Version, x-goog-api-client)
- Add X-Machine-Session-Id header injection
- Align metadata/mode parameters across all Antigravity API calls
- Implement double injection for system prompt (raw + [ignore] wrapped)
- Rename internal anti-loop header to x-request-source for anonymity

Skipped: commit 6 (signature side-channel caching) — kept DEFAULT_THINKING_GEMINI_SIGNATURE
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zx
2026-02-20 14:44:29 +07:00
committed by decolua
parent 9fbd6e619d
commit a229d79158
11 changed files with 309 additions and 121 deletions

View File

@@ -1,6 +1,20 @@
/**
* OAuth Configuration Constants
*/
import { platform, arch } from "os";
/**
* Get the platform enum value based on the current OS.
* Matches Antigravity binary's ClientMetadata.Platform enum.
*/
function getOAuthPlatformEnum() {
const os = platform();
const architecture = arch();
if (os === "darwin") return architecture === "arm64" ? 2 : 1;
if (os === "linux") return architecture === "arm64" ? 4 : 3;
if (os === "win32") return 5;
return 0;
}
// Claude OAuth Configuration (Authorization Code Flow with PKCE)
export const CLAUDE_CONFIG = {
@@ -83,9 +97,17 @@ export const ANTIGRAVITY_CONFIG = {
onboardUserEndpoint: "https://cloudcode-pa.googleapis.com/v1internal:onboardUser",
loadCodeAssistUserAgent: "google-api-nodejs-client/9.15.1",
loadCodeAssistApiClient: "google-cloud-sdk vscode_cloudshelleditor/0.1",
loadCodeAssistClientMetadata: `{"ideType":"IDE_UNSPECIFIED","platform":"PLATFORM_UNSPECIFIED","pluginType":"GEMINI"}`,
loadCodeAssistClientMetadata: JSON.stringify({ ideType: 9, platform: getOAuthPlatformEnum(), pluginType: 2 }),
};
/**
* Get client metadata using numeric enum values for API calls.
* @returns {{ ideType: number, platform: number, pluginType: number }}
*/
export function getOAuthClientMetadata() {
return { ideType: 9, platform: getOAuthPlatformEnum(), pluginType: 2 };
}
// OpenAI OAuth Configuration (Authorization Code Flow with PKCE)
export const OPENAI_CONFIG = {
clientId: "app_EMoamEEZ73f0CkXaXp7hrann",

View File

@@ -14,6 +14,7 @@ import {
GITHUB_CONFIG,
KIRO_CONFIG,
CURSOR_CONFIG,
getOAuthClientMetadata,
} from "./constants/oauth";
// Provider configurations
@@ -184,7 +185,8 @@ const PROVIDERS = {
"Content-Type": "application/json",
},
body: JSON.stringify({
metadata: { ideType: "IDE_UNSPECIFIED", platform: "PLATFORM_UNSPECIFIED", pluginType: "GEMINI" },
metadata: getOAuthClientMetadata(),
mode: 1,
}),
}
);
@@ -254,7 +256,7 @@ const PROVIDERS = {
"X-Goog-Api-Client": ANTIGRAVITY_CONFIG.loadCodeAssistApiClient,
"Client-Metadata": ANTIGRAVITY_CONFIG.loadCodeAssistClientMetadata,
};
const metadata = { ideType: "IDE_UNSPECIFIED", platform: "PLATFORM_UNSPECIFIED", pluginType: "GEMINI" };
const metadata = getOAuthClientMetadata();
// Fetch user info
const userInfoRes = await fetch(`${ANTIGRAVITY_CONFIG.userInfoUrl}?alt=json`, {
@@ -269,7 +271,7 @@ const PROVIDERS = {
const loadRes = await fetch(ANTIGRAVITY_CONFIG.loadCodeAssistEndpoint, {
method: "POST",
headers,
body: JSON.stringify({ metadata }),
body: JSON.stringify({ metadata, mode: 1 }),
});
if (loadRes.ok) {
const data = await loadRes.json();
@@ -295,7 +297,7 @@ const PROVIDERS = {
const onboardRes = await fetch(ANTIGRAVITY_CONFIG.onboardUserEndpoint, {
method: "POST",
headers,
body: JSON.stringify({ tierId, metadata, cloudaicompanionProject: projectId }),
body: JSON.stringify({ tierId, metadata, cloudaicompanionProject: projectId, mode: 1 }),
});
if (onboardRes.ok) {
const result = await onboardRes.json();
@@ -727,9 +729,9 @@ export function generateAuthData(providerName, redirectUri) {
*/
export async function exchangeTokens(providerName, code, redirectUri, codeVerifier, state) {
const provider = getProvider(providerName);
const tokens = await provider.exchangeToken(provider.config, code, redirectUri, codeVerifier, state);
let extra = null;
if (provider.postExchange) {
extra = await provider.postExchange(tokens);
@@ -761,9 +763,9 @@ export async function pollForToken(providerName, deviceCode, codeVerifier, extra
if (provider.flowType !== "device_code") {
throw new Error(`Provider ${providerName} does not support device code flow`);
}
const result = await provider.pollToken(provider.config, deviceCode, codeVerifier, extraData);
if (result.ok) {
// For device code flows, success is only when we have an access token
if (result.data.access_token) {
@@ -777,23 +779,23 @@ export async function pollForToken(providerName, deviceCode, codeVerifier, extra
// Check if it's still pending authorization
if (result.data.error === 'authorization_pending' || result.data.error === 'slow_down') {
// This is not a failure, just still waiting
return {
success: false,
error: result.data.error,
return {
success: false,
error: result.data.error,
errorDescription: result.data.error_description || result.data.message,
pending: result.data.error === 'authorization_pending'
};
} else {
// Actual error
return {
success: false,
error: result.data.error || 'no_access_token',
errorDescription: result.data.error_description || result.data.message || 'No access token received'
return {
success: false,
error: result.data.error || 'no_access_token',
errorDescription: result.data.error_description || result.data.message || 'No access token received'
};
}
}
}
return { success: false, error: result.data.error, errorDescription: result.data.error_description };
}

View File

@@ -1,4 +1,5 @@
import crypto from "crypto";
import { platform, arch } from "os";
import open from "open";
import { ANTIGRAVITY_CONFIG } from "../constants/oauth.js";
import { getServerCredentials } from "../config/index.js";
@@ -92,12 +93,20 @@ export class AntigravityService {
/**
* Get metadata object for API calls
* Uses numeric enum values matching Antigravity binary specifications
*/
getMetadata() {
const os = platform();
const architecture = arch();
let platformEnum = 0; // UNSPECIFIED
if (os === "darwin") platformEnum = architecture === "arm64" ? 2 : 1;
else if (os === "linux") platformEnum = architecture === "arm64" ? 4 : 3;
else if (os === "win32") platformEnum = 5;
return {
ideType: "IDE_UNSPECIFIED",
platform: "PLATFORM_UNSPECIFIED",
pluginType: "GEMINI",
ideType: 9, // ANTIGRAVITY
platform: platformEnum,
pluginType: 2, // GEMINI
};
}
@@ -108,7 +117,7 @@ export class AntigravityService {
const response = await fetch(this.config.loadCodeAssistEndpoint, {
method: "POST",
headers: this.getApiHeaders(accessToken),
body: JSON.stringify({ metadata: this.getMetadata() }),
body: JSON.stringify({ metadata: this.getMetadata(), mode: 1 }),
});
if (!response.ok) {
@@ -117,7 +126,7 @@ export class AntigravityService {
}
const data = await response.json();
// Extract project ID
let projectId = data.cloudaicompanionProject;
if (typeof projectId === 'object' && projectId !== null && projectId.id) {
@@ -149,6 +158,7 @@ export class AntigravityService {
tierId,
metadata: this.getMetadata(),
cloudaicompanionProject: projectId,
mode: 1,
}),
});
@@ -166,7 +176,7 @@ export class AntigravityService {
async completeOnboarding(accessToken, projectId, tierId, maxRetries = 10) {
for (let i = 0; i < maxRetries; i++) {
const result = await this.onboardUser(accessToken, projectId, tierId);
if (result.done === true) {
// Extract final project ID from response
let finalProjectId = projectId;
@@ -301,7 +311,7 @@ export class AntigravityService {
// Load Code Assist to get project ID and tier
const { projectId, tierId } = await this.loadCodeAssist(tokens.access_token);
if (!projectId) {
throw new Error("No Google Cloud Project found. Please ensure you have a GCP project with Gemini Code Assist enabled.");
}

View File

@@ -1,6 +1,6 @@
import crypto from "crypto";
import open from "open";
import { GEMINI_CONFIG } from "../constants/oauth.js";
import { GEMINI_CONFIG, getOAuthClientMetadata } from "../constants/oauth.js";
import { getServerCredentials } from "../config/index.js";
import { startLocalServer } from "../utils/server.js";
import { spinner as createSpinner } from "../utils/ui.js";
@@ -71,18 +71,11 @@ export class GeminiCLIService {
"Content-Type": "application/json",
"User-Agent": "google-api-nodejs-client/9.15.1",
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
"Client-Metadata": JSON.stringify({
ideType: "IDE_UNSPECIFIED",
platform: "PLATFORM_UNSPECIFIED",
pluginType: "GEMINI"
})
"Client-Metadata": JSON.stringify(getOAuthClientMetadata())
},
body: JSON.stringify({
metadata: {
ideType: "IDE_UNSPECIFIED",
platform: "PLATFORM_UNSPECIFIED",
pluginType: "GEMINI"
}
metadata: getOAuthClientMetadata(),
mode: 1
})
}
);
@@ -93,7 +86,7 @@ export class GeminiCLIService {
}
const data = await response.json();
// Extract project ID
let projectId = "";
if (typeof data.cloudaicompanionProject === "string") {

View File

@@ -6,6 +6,7 @@ const { promisify } = require("util");
const os = require("os");
// Configuration
const INTERNAL_REQUEST_HEADER = { name: "x-request-source", value: "local" };
const TARGET_HOST = "daily-cloudcode-pa.googleapis.com";
const LOCAL_PORT = 443;
const ROUTER_URL = "http://localhost:20128/v1/chat/completions";
@@ -174,7 +175,7 @@ const server = https.createServer(sslOptions, async (req, res) => {
if (bodyBuffer.length > 0) saveRequestLog(req.url, bodyBuffer);
// Anti-loop: requests from 9Router bypass interception
if (req.headers["x-9router-source"] === "9router") {
if (req.headers[INTERNAL_REQUEST_HEADER.name] === INTERNAL_REQUEST_HEADER.value) {
return passthrough(req, res, bodyBuffer);
}