diff --git a/open-sse/translator/request/openai-to-gemini.ts b/open-sse/translator/request/openai-to-gemini.ts index 14cc4d4e5e..cb1cedd713 100644 --- a/open-sse/translator/request/openai-to-gemini.ts +++ b/open-sse/translator/request/openai-to-gemini.ts @@ -464,12 +464,17 @@ function openaiToGeminiBase( // Gemini expects the signature on the functionCall part itself. // If we are in a mode where missing signatures cause 400s (and we couldn't find one), - // safely default to the bypass string to protect against 400s. + // safely default to the bypass string to protect against 400s. The bypass sentinel is + // an audit-trail risk (a magic validator-bypass string upstream could log/flag), so + // operators can disable it via ANTIGRAVITY_ALLOW_SIGNATURE_BYPASS=0 — real signatures + // are always preferred; the sentinel only fills the gap when none is available. + const signatureBypassEnabled = + toolNameOptions.supportsSignatureBypass && + signaturelessToolCallMode !== "text" && + process.env.ANTIGRAVITY_ALLOW_SIGNATURE_BYPASS !== "0"; const finalSignature = embeddedThoughtSignature || - (toolNameOptions.supportsSignatureBypass && signaturelessToolCallMode !== "text" - ? "skip_thought_signature_validator" - : undefined); + (signatureBypassEnabled ? "skip_thought_signature_validator" : undefined); parts.push({ ...(finalSignature ? { thoughtSignature: finalSignature } : {}), functionCall: { diff --git a/src/lib/oauth/providers/antigravity.ts b/src/lib/oauth/providers/antigravity.ts index 8456938f82..f3790141c6 100644 --- a/src/lib/oauth/providers/antigravity.ts +++ b/src/lib/oauth/providers/antigravity.ts @@ -112,7 +112,12 @@ async function onboardAntigravityUser( tierId: string, metadata: Record ): Promise { - for (let i = 0; i < 10; i++) { + // Bounded onboarding: cap retries (was 10) and jitter the delay so a stuck + // loop cannot look like scripted automation to the upstream (ban-safety). + const MAX_ONBOARD_RETRIES = 3; + const BASE_RETRY_MS = 3000; + const JITTER_MS = 4000; + for (let i = 0; i < MAX_ONBOARD_RETRIES; i++) { try { const response = await fetchFirstOk( config.onboardUserEndpoints, @@ -124,7 +129,7 @@ async function onboardAntigravityUser( } catch { return; } - await new Promise((resolve) => setTimeout(resolve, 5000)); + await new Promise((resolve) => setTimeout(resolve, BASE_RETRY_MS + Math.random() * JITTER_MS)); } } diff --git a/tests/unit/translator-antigravity-signature-bypass.test.ts b/tests/unit/translator-antigravity-signature-bypass.test.ts new file mode 100644 index 0000000000..f7a3853c86 --- /dev/null +++ b/tests/unit/translator-antigravity-signature-bypass.test.ts @@ -0,0 +1,62 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { openaiToAntigravityRequest } = + await import("../../open-sse/translator/request/openai-to-gemini.ts"); + +const body = { + messages: [ + { role: "user", content: "Use the terminal tool to echo hi." }, + { + role: "assistant", + tool_calls: [ + { + id: "call_1", + type: "function", + function: { name: "terminal", arguments: JSON.stringify({ command: "echo hi" }) }, + }, + ], + }, + { role: "tool", tool_call_id: "call_1", name: "terminal", content: "hi" }, + ], + tools: [ + { + type: "function", + function: { + name: "terminal", + description: "Run a shell command", + parameters: { type: "object", properties: { command: { type: "string" } } }, + }, + }, + ], +}; + +function modelParts(model: string, b: unknown) { + const envelope = openaiToAntigravityRequest(model, b, true) as { + request: { contents: Array<{ role: string; parts: Array> }> }; + }; + const modelMsg = envelope.request.contents.find((c) => c.role === "model"); + assert.ok(modelMsg, "expected an assistant (model-role) message in the translation"); + return modelMsg.parts; +} + +test("antigravity multi-turn tool call carries the signature bypass sentinel by default", () => { + const parts = modelParts("gemini-3.1-pro-low", body); + const fc = parts.find((p) => p.functionCall); + assert.ok(fc, "expected a functionCall part"); + assert.equal(fc.thoughtSignature, "skip_thought_signature_validator"); +}); + +test("ANTIGRAVITY_ALLOW_SIGNATURE_BYPASS=0 disables the sentinel", () => { + const prev = process.env.ANTIGRAVITY_ALLOW_SIGNATURE_BYPASS; + process.env.ANTIGRAVITY_ALLOW_SIGNATURE_BYPASS = "0"; + try { + const parts = modelParts("gemini-3.1-pro-low", body); + const fc = parts.find((p) => p.functionCall); + assert.ok(fc, "expected a functionCall part"); + assert.equal(fc.thoughtSignature, undefined); + } finally { + if (prev === undefined) delete process.env.ANTIGRAVITY_ALLOW_SIGNATURE_BYPASS; + else process.env.ANTIGRAVITY_ALLOW_SIGNATURE_BYPASS = prev; + } +});