From 32935b04fa9a429ecc9236c00cb1e6c96b4a12e1 Mon Sep 17 00:00:00 2001 From: Benson K B Date: Mon, 10 Aug 2026 12:09:39 +0530 Subject: [PATCH] =?UTF-8?q?fix(antigravity):=20ban-safety=20hardening=20?= =?UTF-8?q?=E2=80=94=20bounded=20onboarding=20retries,=20gate=20thought-si?= =?UTF-8?q?gnature=20bypass=20sentinel=20(#9939)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(deps): bump nanoid, dompurify for 2 new Dependabot alerts (#189, #190) Bumps: nanoid ^3.3.17 (was transitive, now overridden), dompurify ^3.4.13 (with monaco-editor scoped override). Closes Dependabot #189, #190. Remaining #182-#188 (js-yaml + mermaid) already closed by #9651 merge — awaiting Dependabot re-scan. npm audit → 0 vulnerabilities. * fix(repo): harden .gitignore to also ignore a _tasks symlink (/_tasks) _tasks is a SEPARATE nested git repo (gitignored). The pattern _tasks/ (trailing slash) ignores only a directory, not a SYMLINK named _tasks. A self-referential _tasks symlink can slip in via git add -A and, once pulled, checkout materializes it over the real _tasks repo (destroying plans/specs/hands-off). Anchored /_tasks ignores the symlink too, preventing re-capture. * fix(antigravity): ban-safety hardening — bounded onboarding retries with jitter, gate the thought-signature bypass sentinel - onboardAntigravityUser: cap retries 10->3 and jitter the delay (3-7s) so a stuck loop cannot read as scripted automation to the upstream - openai-to-gemini: the skip_thought_signature_validator sentinel is an audit-trail risk; gate it behind ANTIGRAVITY_ALLOW_SIGNATURE_BYPASS (default enabled for compatibility, set 0 to disable). Real signatures always win. * test(antigravity): cover the signature-bypass sentinel gate (default on, env-disabled) Adds tests/unit/translator-antigravity-signature-bypass.test.ts (2 tests, verified locally with node --import tsx/esm) + CHANGELOG entry for the ban-safety hardening. --------- Co-authored-by: diegosouzapw Co-authored-by: Diego Rodrigues de Sa e Souza Co-authored-by: diegosouzapw Co-authored-by: benzntech Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --- .../translator/request/openai-to-gemini.ts | 13 ++-- src/lib/oauth/providers/antigravity.ts | 9 ++- ...lator-antigravity-signature-bypass.test.ts | 62 +++++++++++++++++++ 3 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 tests/unit/translator-antigravity-signature-bypass.test.ts 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; + } +});