From 7eb305685668ced07377860caa71c505701713ea Mon Sep 17 00:00:00 2001 From: Ravi Tharuma Date: Mon, 13 Apr 2026 11:41:22 +0200 Subject: [PATCH] feat: update Gemini CLI executor with dynamic UA, header scrubbing, and obfuscation Matches the Antigravity executor treatment: - Dynamic User-Agent: GeminiCLI/0.31.0/MODEL (OS; ARCH) per-model - X-Goog-Api-Client: maintained (was already correct) - Header scrubbing: removes proxy/fingerprint headers - Sensitive word obfuscation in user message content - Tracks current model for per-request UA generation --- open-sse/executors/gemini-cli.ts | 38 +++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/open-sse/executors/gemini-cli.ts b/open-sse/executors/gemini-cli.ts index 3c8298a0b6..228b00b38c 100644 --- a/open-sse/executors/gemini-cli.ts +++ b/open-sse/executors/gemini-cli.ts @@ -1,5 +1,8 @@ import { BaseExecutor } from "./base.ts"; import { PROVIDERS, OAUTH_ENDPOINTS } from "../config/constants.ts"; +import { geminiCLIUserAgent, googApiClientHeader } from "../services/antigravityHeaders.ts"; +import { scrubProxyAndFingerprintHeaders } from "../services/antigravityHeaderScrub.ts"; +import { obfuscateSensitiveWords } from "../services/antigravityObfuscation.ts"; const LOAD_CODE_ASSIST_URL = "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist"; const PROJECT_TTL_MS = 30_000; // 30 seconds — matches native Gemini CLI @@ -22,19 +25,20 @@ export class GeminiCLIExecutor extends BaseExecutor { } buildHeaders(credentials, stream = true) { - return { + const raw = { "Content-Type": "application/json", Authorization: `Bearer ${credentials.accessToken}`, - // Fingerprint headers matching native GeminiCLI client (prevents upstream rejection) - "User-Agent": "GeminiCLI/0.31.0/unknown (linux; x64)", - "X-Goog-Api-Client": "google-genai-sdk/1.41.0 gl-node/v22.19.0", + // Dynamic headers matching native GeminiCLI client + "User-Agent": geminiCLIUserAgent(this._currentModel || "unknown"), + "X-Goog-Api-Client": googApiClientHeader(), ...(stream && { Accept: "text/event-stream" }), - // NOTE: x-goog-user-project removed — the stored projectId can become stale for - // free-tier accounts, causing 403 "Cloud Code Private API has not been used in - // project X". The API resolves the correct project from the OAuth token alone. }; + return scrubProxyAndFingerprintHeaders(raw); } + // Track current model for dynamic UA (set by transformRequest) + private _currentModel = "unknown"; + /** * Fetch the current cloudaicompanionProject via loadCodeAssist API. * Native Gemini CLI refreshes this every 30 seconds — OmniRoute stores it once @@ -134,15 +138,29 @@ export class GeminiCLIExecutor extends BaseExecutor { } async transformRequest(model, body, stream, credentials) { + // Track model for dynamic User-Agent + this._currentModel = model || "unknown"; + // Refresh the project ID via loadCodeAssist (cached for 30s). - // The translator builds the envelope with the stale stored projectId — - // we replace it here with the fresh one before sending to the API. if (body && typeof body === "object" && body.request && credentials.accessToken) { const freshProject = await this.refreshProject(credentials.accessToken); if (freshProject) { body.project = freshProject; } - // If refresh failed, keep the stale projectId as a best-effort fallback + + // Obfuscate sensitive client names in user content + const contents = body.request?.contents; + if (Array.isArray(contents)) { + for (const msg of contents) { + if (Array.isArray(msg.parts)) { + for (const part of msg.parts) { + if (typeof part.text === "string") { + part.text = obfuscateSensitiveWords(part.text); + } + } + } + } + } } return body; }