fix(antigravity): ban-safety hardening — bounded onboarding retries, gate thought-signature bypass sentinel (#9939)

* 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 <diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@outlook.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
Co-authored-by: benzntech <benzntech@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
Benson K B
2026-08-10 12:09:39 +05:30
committed by GitHub
parent 9105220242
commit 32935b04fa
3 changed files with 78 additions and 6 deletions

View File

@@ -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: {

View File

@@ -112,7 +112,12 @@ async function onboardAntigravityUser(
tierId: string,
metadata: Record<string, string>
): Promise<void> {
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));
}
}

View File

@@ -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<Record<string, unknown>> }> };
};
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;
}
});