From ac0d1888c04e26eb799b903cd3099a5169854776 Mon Sep 17 00:00:00 2001 From: sususu Date: Wed, 19 Aug 2026 19:13:56 +0800 Subject: [PATCH] fix(gemini): strip 'encrypted' metadata from tool parameters schemas (#5065) --- .../antigravity_schema_sanitize_test.go | 21 +++++ internal/util/gemini_schema.go | 2 +- internal/util/gemini_schema_test.go | 86 +++++++++++++++++++ 3 files changed, 108 insertions(+), 1 deletion(-) diff --git a/internal/runtime/executor/antigravity_schema_sanitize_test.go b/internal/runtime/executor/antigravity_schema_sanitize_test.go index 1a368b44f..381668539 100644 --- a/internal/runtime/executor/antigravity_schema_sanitize_test.go +++ b/internal/runtime/executor/antigravity_schema_sanitize_test.go @@ -592,3 +592,24 @@ func TestAntigravityBuildRequestStripsPropertyNamesFromOutboundBody(t *testing.T } } } + +// TestSanitizeAntigravityRequestSchemasStripsEncryptedMetadata covers Codex client tool parameters +// that carry "encrypted": true or "encrypted": false markers. +func TestSanitizeAntigravityRequestSchemasStripsEncryptedMetadata(t *testing.T) { + encryptedSchema := `{"type":"object","properties":{"key":{"type":"string","encrypted":true},"timeout":{"type":"integer","encrypted":false}},"required":["key"]}` + + for _, declContainer := range []string{"functionDeclarations", "function_declarations"} { + payload := `{"request":{"tools":[{"` + declContainer + `":[{"name":"test_tool","parameters":` + encryptedSchema + `}]}]}}` + + for _, useAntigravitySchema := range []bool{false, true} { + got := sanitizeAntigravityRequestSchemas(payload, useAntigravitySchema) + if strings.Contains(got, `"encrypted"`) { + t.Errorf("declContainer=%s antigravity=%v: 'encrypted' marker survived sanitization: %s", declContainer, useAntigravitySchema, got) + } + schema := gjson.Get(got, "request.tools.0."+declContainer+".0.parameters") + if !schema.Get("properties.key.type").Exists() || schema.Get("properties.key.type").String() != "string" { + t.Errorf("declContainer=%s antigravity=%v: key property was corrupted: %s", declContainer, useAntigravitySchema, schema.Raw) + } + } + } +} diff --git a/internal/util/gemini_schema.go b/internal/util/gemini_schema.go index 48b6615ea..ab36c7849 100644 --- a/internal/util/gemini_schema.go +++ b/internal/util/gemini_schema.go @@ -766,7 +766,7 @@ func removeUnsupportedKeywords(jsonStr string, options jsonSchemaCleanOptions) s "$schema", "$defs", "definitions", "const", "$ref", "$id", "additionalProperties", "propertyNames", "patternProperties", // Gemini doesn't support these schema keywords "if", "then", "else", - "$comment", "enumDescriptions", "enumTitles", "prefill", "deprecated", // Schema metadata fields unsupported by Gemini + "$comment", "enumDescriptions", "enumTitles", "prefill", "deprecated", "encrypted", // Schema metadata fields unsupported by Gemini ) if options.antigravitySemantics { keywords = append(keywords, "not") diff --git a/internal/util/gemini_schema_test.go b/internal/util/gemini_schema_test.go index 4cea33977..526554eac 100644 --- a/internal/util/gemini_schema_test.go +++ b/internal/util/gemini_schema_test.go @@ -1532,3 +1532,89 @@ func TestSortByDepthUsesSegmentsAndIsStable(t *testing.T) { t.Fatalf("sortByDepth() = %v, want %v", paths, want) } } + +// TestCleanJSONSchemaStripsEncryptedMetadata covers Codex client tool definitions where +// properties carry the Responses-only "encrypted" marker (e.g. "encrypted": true or "encrypted": false). +// The Gemini backend strictly rejects unknown schema fields with an INVALID_ARGUMENT 400. +func TestCleanJSONSchemaStripsEncryptedMetadata(t *testing.T) { + input := `{ + "type": "object", + "properties": { + "api_key": { + "type": "string", + "description": "API credential", + "encrypted": true + }, + "timeout": { + "type": "integer", + "encrypted": false + }, + "nested": { + "type": "object", + "properties": { + "secret": { + "type": "string", + "encrypted": true + } + } + } + }, + "required": ["api_key"] + }` + + for cleaner, clean := range map[string]func(string) string{ + "antigravity": CleanJSONSchemaForAntigravity, + "gemini": CleanJSONSchemaForGemini, + "antigravityTool": func(s string) string { return CleanJSONSchemaForAntigravityTool(s, false) }, + "antigravityResponse": CleanJSONSchemaForAntigravityResponse, + } { + got := clean(input) + if strings.Contains(got, `"encrypted"`) { + t.Errorf("%s: 'encrypted' marker survived cleaning: %s", cleaner, got) + } + parsed := gjson.Parse(got) + if !parsed.Get("properties.api_key.type").Exists() || parsed.Get("properties.api_key.description").String() != "API credential" { + t.Errorf("%s: api_key schema was corrupted: %s", cleaner, got) + } + if !parsed.Get("properties.nested.properties.secret.type").Exists() { + t.Errorf("%s: nested property secret was corrupted: %s", cleaner, got) + } + } +} + +// TestCleanJSONSchemaKeepsPropertyNamedEncrypted guards the legitimate case where a tool +// parameter itself is named "encrypted" (e.g. properties.encrypted: {"type": "boolean"}). +func TestCleanJSONSchemaKeepsPropertyNamedEncrypted(t *testing.T) { + input := `{ + "type": "object", + "properties": { + "encrypted": { + "type": "boolean", + "description": "Whether the payload is encrypted", + "encrypted": true + }, + "data": { + "type": "string" + } + }, + "required": ["encrypted"] + }` + + for cleaner, clean := range map[string]func(string) string{ + "antigravity": CleanJSONSchemaForAntigravity, + "gemini": CleanJSONSchemaForGemini, + } { + got := clean(input) + parsed := gjson.Parse(got) + if !parsed.Get("properties.encrypted").Exists() { + t.Errorf("%s: property named 'encrypted' was removed: %s", cleaner, got) + } + if parsed.Get("properties.encrypted.type").String() != "boolean" { + t.Errorf("%s: property named 'encrypted' type corrupted: %s", cleaner, got) + } + // The inner attribute "encrypted": true must be stripped + if parsed.Get("properties.encrypted.encrypted").Exists() { + t.Errorf("%s: inner 'encrypted' attribute survived: %s", cleaner, got) + } + } +}