fix(gemini): strip 'encrypted' metadata from tool parameters schemas (#5065)

This commit is contained in:
sususu
2026-08-19 19:13:56 +08:00
parent 5fef17e2ec
commit ac0d1888c0
3 changed files with 108 additions and 1 deletions

View File

@@ -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)
}
}
}
}

View File

@@ -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")

View File

@@ -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)
}
}
}