Skip to content

feat(antigravity): implement multi-layer detection evasion (TLS JA3, session headers, and payload fingerprinting) #1138

Description

@rodtrevizan

Context

The Antigravity / Cloud Code Assist API (cloudcode-pa.googleapis.com/v1internal) is Google's backend powering the Antigravity coding agent. 9router already supports it as a provider with a growing stack: an OAuth2 flow, MITM DNS redirect for the desktop IDE, model aliasing, and a dedicated executor that translates OpenAI/Claude-format requests into Gemini v1internal payloads.

However, Google deploys multiple layered detection checks — beyond just User-Agent — including TLS fingerprint correlation (JA3/JA4), header and payload pattern validation, thought_signature injection, state.vscdb/device identity sync, and trace-ID relay. When any layer is missing or inconsistent, the backend returns 403 Forbidden, VALIDATION_REQUIRED (temporary block), or silent ToS bans.

Two sibling projects — Antigravity-Manager (Rust) and Antigravity-Tools-LS (Rust language server) — have already reverse-engineered most of these layers and documented exactly what the Google backend validates, and what a third-party client must do to stay under the radar.

Current Problem

The 9router Antigravity executor (open-sse/executors/antigravity.js) covers the basics but misses several critical detection vectors that the Rust projects have mapped and mitigate. The table below summarizes the gaps:

Detection Layer What Google validates 9router today Reference (Manager/Tools-LS)
TLS/JA3 fingerprint Correlates JA3/JA4 with User-Agent; mismatched fingerprint → 403/Captcha Plain Node.js TLS stack, no Chrome impersonation src-tauri/src/utils/http.rsEmulation::Chrome123 via rquest + BoringSSL
OAuth TLS separation Token exchange (/token, /oauth2) expects pure native TLS fingerprint, NOT Chrome-emulated Same fetch() for all endpoints, no OAuth-vs-agent distinction src-tauri/src/modules/oauth.rsget_long_standard_client() (no JA3 emulation)
Session headers Requires x-machine-id, x-vscode-sessionid, x-goog-user-project; absence = 403 SERVICE_DISABLED None of these injected; only X-Machine-Session-Id (different key) src-tauri/src/proxy/upstream/client.rs — injection of all three headers
requestId format Expects strict agent/{timestamp_ms}/{hex8} format (commented "official" in source) Uses agent-${crypto.randomUUID()} — format mismatch src-tauri/src/proxy/mappers/gemini/wrapper.rs — line 540 area
enabledCreditTypes Requires ["GOOGLE_ONE_AI"] in agent requests to identify as official client Not injected at all src-tauri/src/proxy/mappers/gemini/wrapper.rs — lines 568-588
Dynamic ideType / userAgent Enterprise accounts (jetski/JETSKI) vs regular (antigravity/ANTIGRAVITY) based on email domain Always hardcoded "antigravity" / ANTIGRAVITY src-tauri/src/proxy/mappers/gemini/wrapper.rs — lines 546-566
thought_signature Required for Gemini 3+ models; minimum length enforced; missing or too short → error No signature capture/replay; no sentinel injection signature_store.rs + claude/request.rs — sentinel skip_thought_signature_validator
state.vscdb / serviceMachineId sync Mismatch between reported machine identity and SQLite state → "Environment Changed" popup, session revocation No interaction with IDE SQLite database transcoder-core/src/ide.rs — atomic writes to state.vscdb
__cloudCodeMeta trace relay Broken trace chain (proxy drops response metadata) signals interception No explicit relay of __cloudCodeMeta or x-cloudaicompanion-trace-id handlers/openai.rs + gemini.rs
Version fingerprint Reported version must match behavioral fingerprint of ls_core binary Hardcoded 1.107.0; does not dynamically align commands/mod.rs — dynamic version detection
CSRF token in gRPC ls_core requires x-codeium-csrf-token on every gRPC Request; missing = 403 Not applicable (9router doesn't run ls_core) cascade/client.rs

Additionally, the loadCodeAssist OAuth metadata in src/lib/oauth/constants/oauth.js uses IDE_UNSPECIFIED / PLATFORM_UNSPECIFIED as a hardcoded string, while the open-sse path uses numeric enum values (ideType: 9 / ANTIGRAVITY). This inconsistency may trigger validation on Google's side depending on which code path runs during OAuth.

Proposal

The suggestions below are grouped by priority level. P0 items directly cause 403s/bans today. P1 items improve long-term session stability. P2 items are nice-to-have hardening.


P0 — Critical (currently causing 403 / VALIDATION_REQUIRED / bans)

1. Inject enabledCreditTypes: ["GOOGLE_ONE_AI"] in agent payload

File to modify: open-sse/executors/antigravity.js (transformRequest)

Reference: gemini/wrapper.rs lines 568-588 — wraps the body object with this field when request_type !== "image_gen".

Suggested implementation:

// Inside transformRequest(), after building the base body:
if (body.requestType !== "image_gen") {
  body.enabledCreditTypes = ["GOOGLE_ONE_AI"];
}

2. Fix requestId format to agent/{timestamp_ms}/{hex8}

File to modify: open-sse/executors/antigravity.js (transformRequest)

Reference: gemini/wrapper.rs — line ~540 area.

Suggested implementation:

const now = Date.now();
const hex8 = crypto.randomBytes(4).toString("hex");
body.requestId = `agent/${now}/${hex8}`;

3. Emulate Chrome 123 TLS fingerprint on upstream agent calls

Files to modify: open-sse/executors/antigravity.js (or a shared HTTP helper)

Reference: http.rsEmulation::Chrome123 via rquest + BoringSSL.

Suggested approaches (Node.js):

  • Option A (lightweight): Use curl-impersonate as a subprocess wrapper for upstream calls, specifying Chrome 123.
  • Option B (native): Use got-scraping which wraps uTLS (a Go TLS library ported to Node) to set Chrome cipher suites and extensions.
  • Option C (custom): Override tls.DEFAULT_CIPHERS and tls.DEFAULT_ECDH_CURVE in Node.js to match Chrome 123's TLS parameters before each upstream fetch. This is partial but zero-dependency.

4. Inject x-machine-id, x-vscode-sessionid, x-goog-user-project headers

Files to modify: open-sse/executors/antigravity.js (buildHeaders) + open-sse/config/appConstants.js

Reference: upstream/client.rs — injection of all session headers and project header.

Suggested implementation:

// In buildHeaders():
headers["x-machine-id"] = machineId;         // derive from node-machine-id or from credentials
headers["x-vscode-sessionid"] = sessionId;   // already have X-Machine-Session-Id; rename key
headers["x-goog-user-project"] = credentials.projectId;

Also add retry logic: if upstream returns 403, retry without x-goog-user-project (matching Manager behavior documented in upstream/client.rs).


5. Separate OAuth token exchange from agent TLS profile

Files to modify: open-sse/executors/antigravity.js (refreshCredentials) + src/lib/oauth/services/antigravity.js

Reference: oauth.rs — uses get_long_standard_client() (no JA3 emulation) for token calls.

Suggested implementation: For refreshCredentials() and the OAuth token exchange in src/lib/oauth/services/antigravity.js, use the native Node TLS stack (current default), but when P0 item 3 is implemented for agent calls, ensure OAuth calls bypass the Chrome emulation layer.


P1 — Stability improvement (reduces intermittent 403s)

6. Dynamic ideType / userAgent based on account email domain

File to modify: open-sse/executors/antigravity.js (transformRequest)

Reference: gemini/wrapper.rs lines 546-566.

If the account email is a known Enterprise/Workspace domain (e.g., not @gmail.com), set body.userAgent = "jetski" and inject body.metadata.ideType = "JETSKI".


7. thought_signature capture and replay

Files to modify: open-sse/executors/antigravity.js (stream handler) + new shared store file

Reference: signature_store.rs (global Mutex<HashMap> for signatures) + openai/streaming.rs (capture from thoughtSignature in SSE parts).

Capture thought_signature from streaming responses (SSE thoughtSignature or thought_signature fields in parts). On subsequent requests, inject the captured signature as thoughtSignature: "<captured>". If no signature is cached, inject the sentinel "skip_thought_signature_validator" to prevent Gemini 3+ from rejecting the call entirely.


8. Relay __cloudCodeMeta through to the downstream client

File to modify: open-sse/executors/antigravity.js (SSE response handling)

Reference: handlers/openai.rs — relays __cloudCodeMeta from upstream response.

When streaming from Gemini, read the x-cloudaicompanion-trace-id response header and inject a __cloudCodeMeta object (with traceId) into the outgoing SSE stream or the final JSON response object. This maintains the telemetry chain that the Google backend expects clients to preserve.


P2 — Nice to have (further hardening)

9. Synchronize storage.serviceMachineId with the IDE's state.vscdb

Reference: ide.rs — atomic INSERT OR REPLACE into ItemTable on macOS/Linux/Windows.

For MITM mode specifically: after the user authenticates, open the VS Code / Cursor state.vscdb SQLite database and write the OAuth-derived machine identity into the storage.serviceMachineId key. This prevents the "Environment Changed" popup on the IDE side.


10. Dynamic version detection (avoid hardcoded 1.107.0)

File to modify: open-sse/config/appConstants.js (ANTIGRAVITY_HEADERS / getPlatformUserAgent)

Reference: commands/mod.rs (get_antigravity_path) — detects local extension version.

If a local Antigravity IDE installation is detected (via MITM), read the extension version from its manifest and construct the User-Agent string dynamically rather than hardcoding 1.104.0 or 1.107.0.


11. Fix OAuth Client-Metadata consistency

File to modify: src/lib/oauth/constants/oauth.js

The loadCodeAssistClientMetadata is a hardcoded string "IDE_UNSPECIFIED/PLATFORM_UNSPECIFIED" while the open-sse layer uses numeric enums (ideType: 9). Align both to use the numeric enum format (matching the Protobuf wire format).

Benefits

  • Drastically fewer 403 Forbidden responses and VALIDATION_REQUIRED blocks, since multiple validation layers now pass.
  • Longer session lifetimes — avoiding "Environment Changed" revocations and OAuth token invalidation.
  • Enterprise account compatibility — Workspace/Corporate accounts (which use JETSKI identity) stop failing due to ANTIGRAVITY mismatch.
  • Gemini 3+ model supportthought_signature injection unblocks models that previously errored on missing signatures.
  • Consistency with sibling projects — aligns 9router's spoofing depth with the established patterns in Antigravity-Manager and Antigravity-Tools-LS.
  • Zero breaking changes for users — all proposals are additions/injections to existing requests; no API surface change.

Sustainability: beyond porting, toward independent reverse engineering

The patterns documented in Antigravity-Manager and Antigravity-Tools-LS are an excellent starting point, but porting them verbatim is brittle. The Manager already shows model-specific breakagegemini-3.1-pro-high fails while gemini-3.1-pro-low, Flash variants, and older models continue working — suggesting Google tunes detection per model family.

To build a 9router Antigravity executor that survives Google-side updates without chasing external Rust projects, contributors should follow this independent loop:

  1. Acquire fresh official binaries. Download the latest Antigravity IDE installer (macOS .dmg, Windows .exe, Linux .deb) from the official distribution channel.

  2. Extract and inspect the language server. Unpack the installer, locate ls_core (or language_server_*), and run strings + grep for internal structures: protobuf message names, header constants, and validation logic — especially thought_signature, requestId, enabledCreditTypes, ideType, __cloudCodeMeta.

  3. Analyze the local state database. Open state.vscdb from the IDE's global storage folder with a SQLite viewer + protobuf compiler to decode storage.serviceMachineId, device fingerprint, and OAuth metadata schema.

  4. Capture real traffic. Run the official Antigravity IDE through the 9router MITM while executing a gemini-3.1-pro-high request. Record the full cycle: headers, payload shape, TLS fingerprint, trace IDs, and thought_signature values.

  5. Diff working vs failing models. Repeat the capture with gemini-3.1-pro-low or Flash (working) and compare against gemini-3.1-pro-high (failing). Isolate model-specific differences: signature length, thinking block structure, credit type requirements.

  6. Validate incrementally. Implement one detection layer at a time in open-sse/executors/antigravity.js, test against gemini-3.1-pro-high, and observe whether 403/VALIDATION_REQUIRED disappears. Do not assume a field that works today will work tomorrow — re-capture after every Google-side update.

This process decouples 9router from the current (and sometimes incomplete) state of external projects and enables faster reaction when Google ships changes that break specific models.

References

All detection evidence and countermeasure patterns were gathered from a deep source-code audit of these repositories (HEAD as of 2026-05-14):

Additional files from Antigravity-Manager referenced throughout the issue:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions