From 64babcac7a63658af9c75dcc0e8a71ec341df76a Mon Sep 17 00:00:00 2001 From: Sebastian Barrios Date: Sat, 10 Jan 2026 11:02:06 -0500 Subject: [PATCH 1/2] fix(agents): harden Cloud Code Assist compatibility - Expand schema scrubber to strip additional constraint keywords rejected by Cloud Code Assist (examples, minLength, maxLength, minimum, maximum, multipleOf, pattern, format, minItems, maxItems, uniqueItems, minProperties, maxProperties) - Extend tool call ID sanitization to cover toolUse and toolCall block types (previously only functionCall was sanitized) - Update pi-tools test to include 'examples' in unsupported keywords Fixes 400 errors when using google-antigravity/claude-opus-4-5-thinking: - tools.N.custom.input_schema: JSON schema is invalid - messages.N.content.N.tool_use.id: String should match pattern --- src/agents/pi-embedded-helpers.ts | 20 +++++++++++--------- src/agents/pi-tools.test.ts | 1 + src/agents/schema/clean-for-gemini.ts | 25 +++++++++++++++++++++---- 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/agents/pi-embedded-helpers.ts b/src/agents/pi-embedded-helpers.ts index cae4ba51f..049647ac5 100644 --- a/src/agents/pi-embedded-helpers.ts +++ b/src/agents/pi-embedded-helpers.ts @@ -146,18 +146,20 @@ export async function sanitizeSessionMessagesImages( // Also sanitize tool call IDs in assistant messages (function call blocks) const sanitizedContent = await Promise.all( filteredContent.map(async (block) => { - if ( - block && - typeof block === "object" && - (block as { type?: unknown }).type === "functionCall" && - (block as { id?: unknown }).id - ) { - const functionBlock = block as { type: string; id: string }; + if (!block || typeof block !== "object") return block; + + const type = (block as { type?: unknown }).type; + const id = (block as { id?: unknown }).id; + if (typeof id !== "string" || !id) return block; + + // Cloud Code Assist tool blocks require ids matching ^[a-zA-Z0-9_-]+$. + if (type === "functionCall" || type === "toolUse" || type === "toolCall") { return { - ...functionBlock, - id: sanitizeToolCallId(functionBlock.id), + ...((block as unknown) as Record), + id: sanitizeToolCallId(id), }; } + return block; }), ); diff --git a/src/agents/pi-tools.test.ts b/src/agents/pi-tools.test.ts index c3c207c70..a4eabc81f 100644 --- a/src/agents/pi-tools.test.ts +++ b/src/agents/pi-tools.test.ts @@ -384,6 +384,7 @@ describe("createClawdbotCodingTools", () => { "$ref", "$defs", "definitions", + "examples", ]); const findUnsupportedKeywords = ( diff --git a/src/agents/schema/clean-for-gemini.ts b/src/agents/schema/clean-for-gemini.ts index e84729f8a..219319fb8 100644 --- a/src/agents/schema/clean-for-gemini.ts +++ b/src/agents/schema/clean-for-gemini.ts @@ -10,6 +10,23 @@ const UNSUPPORTED_SCHEMA_KEYWORDS = new Set([ "$ref", "$defs", "definitions", + // Non-standard (OpenAPI) keyword; Claude validators reject it. + "examples", + + // Cloud Code Assist appears to validate tool schemas more strictly/quirkily than + // draft 2020-12 in practice; these constraints frequently trigger 400s. + "minLength", + "maxLength", + "minimum", + "maximum", + "multipleOf", + "pattern", + "format", + "minItems", + "maxItems", + "uniqueItems", + "minProperties", + "maxProperties", ]); // Check if an anyOf/oneOf array contains only literal values that can be flattened. @@ -134,14 +151,14 @@ function cleanSchemaForGeminiWithDefs( const result: Record = { ...(cleaned as Record), }; - for (const key of ["description", "title", "default", "examples"]) { + for (const key of ["description", "title", "default"]) { if (key in obj && obj[key] !== undefined) result[key] = obj[key]; } return result; } const result: Record = {}; - for (const key of ["description", "title", "default", "examples"]) { + for (const key of ["description", "title", "default"]) { if (key in obj && obj[key] !== undefined) result[key] = obj[key]; } return result; @@ -157,7 +174,7 @@ function cleanSchemaForGeminiWithDefs( type: flattened.type, enum: flattened.enum, }; - for (const key of ["description", "title", "default", "examples"]) { + for (const key of ["description", "title", "default"]) { if (key in obj && obj[key] !== undefined) result[key] = obj[key]; } return result; @@ -171,7 +188,7 @@ function cleanSchemaForGeminiWithDefs( type: flattened.type, enum: flattened.enum, }; - for (const key of ["description", "title", "default", "examples"]) { + for (const key of ["description", "title", "default"]) { if (key in obj && obj[key] !== undefined) result[key] = obj[key]; } return result; From 0d98e93253f2e1e98d3a345e8cc51b52e8b32db8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 10 Jan 2026 18:03:43 +0100 Subject: [PATCH 2/2] fix: harden cloud code assist tool schema sanitizing (#665) (thanks @sebslight) --- CHANGELOG.md | 1 + README.md | 24 ++++++++++--------- scripts/clawtributors-map.json | 1 + src/agents/pi-embedded-helpers.test.ts | 31 ++++++++++++++++++++++++ src/agents/pi-embedded-helpers.ts | 16 +++++++++++-- src/agents/pi-tools.test.ts | 33 +++++++++++++++++++++++--- 6 files changed, 90 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c0f45586..3ce542edf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - iMessage: fix reasoning persistence across DMs; avoid partial/duplicate replies when reasoning is enabled. (#655) — thanks @antons. - Models/Auth: allow MiniMax API configs without `models.providers.minimax.apiKey` (auth profiles / `MINIMAX_API_KEY`). (#656) — thanks @mneves75. - Agents: avoid duplicate replies when the message tool sends. (#659) — thanks @mickahouan. +- Agents: harden Cloud Code Assist tool ID sanitization (toolUse/toolCall/toolResult) and scrub extra JSON Schema constraints. (#665) — thanks @sebslight. - Agents/Tools: resolve workspace-relative Read/Write/Edit paths; align bash default cwd. (#642) — thanks @mukhtharcm. - Tests/Agents: add regression coverage for workspace tool path resolution and bash cwd defaults. - iOS/Android: enable stricter concurrency/lint checks; fix Swift 6 strict concurrency issues + Android lint errors (ExifInterface, obsolete SDK check). (#662) — thanks @KristijanJovanovski. diff --git a/README.md b/README.md index e1bc9c20a..e9ef6423d 100644 --- a/README.md +++ b/README.md @@ -458,16 +458,18 @@ Thanks to all clawtributors:

steipete joaohlisboa mneves75 rahthakor joshp123 mukhtharcm maxsumrall xadenryan Tobias Bischoff hsrvc - jamesgroat NicholasSpisak dantelex daveonkels Eng. Juan Combetto Mariano Belinky julianengel claude sreekaransrinath dbhurley - gupsammy nachoiacovino Vasanth Rao Naik Sabavat jeffersonwarrior lc0rp scald andranik-sahakyan nachx639 sircrumpet rafaelreis-r - meaningfool ratulsarna lutr0 abhisekbasu1 emanuelst thewilloftheshadow osolmaz kiranjd onutc CashWilliams - sheeek manuelhettich minghinmatthewlam magimetal buddyh mcinteerj timkrase azade-c blacksmith-sh[bot] imfing - petter-b RandyVentures Yurii Chukhlib jalehman obviyus dan-dr iamadig koala73 manmal ogulcancelik - VACInc zats Django Navarro L36 Server neist pcty-nextgen-service-account Syhids erik-agens erikpr1994 fcatuhe - jayhickey jonasjancarik Jonathan D. Rhyne (DJ-D) jverdi Keith the Silly Goose Kit mitschabaude-bot ngutman oswalpalash p6l-richard - philipp-spiess pkrmf Sash Catanzarite VAC alejandro maza andrewting19 antons Asleep123 bjesuiter bolismauro + magimetal jamesgroat NicholasSpisak dantelex daveonkels radek-paclt Eng. Juan Combetto Mariano Belinky julianengel claude + jeffersonwarrior sreekaransrinath dbhurley gupsammy nachoiacovino Vasanth Rao Naik Sabavat lc0rp scald andranik-sahakyan nachx639 + sircrumpet rafaelreis-r meaningfool ratulsarna lutr0 abhisekbasu1 emanuelst thewilloftheshadow KristijanJovanovski osolmaz + kiranjd sebslight onutc CashWilliams sheeek manuelhettich minghinmatthewlam buddyh mcinteerj timkrase + azade-c Yurii Chukhlib austinm911 blacksmith-sh[bot] imfing jarvis-medmatic mahmoudashraf93 petter-b RandyVentures jalehman + jonasjancarik obviyus dan-dr iamadig koala73 manmal neist ogulcancelik pasogott VACInc + zats antons Django Navarro L36 Server pcty-nextgen-service-account Syhids erik-agens erikpr1994 fcatuhe HeimdallStrategy + henrino3 jayhickey Jonathan D. Rhyne (DJ-D) jverdi Keith the Silly Goose Kit mitschabaude-bot ngutman oswalpalash p6l-richard + philipp-spiess pkrmf Sash Catanzarite VAC adam91holt alejandro maza andrewting19 Asleep123 bjesuiter bolismauro cash-echo-bot Clawd conhecendocontato gtsifrikas HazAT hrdwdmrbl hugobarauna Jarvis kitze kkarimi - loukotal martinpucik Miles mrdbstn MSch nexty5870 prathamdby reeltimeapps RLTCmpe Rolf Fredheim - snopoke wstock YuriNachos Azade ddyo Erik latitudeki5223 Manuel Maly Mourad Boustani pcty-nextgen-ios-builder - Quentin Randy Torres William Stock zknicker + levifig Lloyd loukotal martinpucik mickahouan Miles mrdbstn MSch nexty5870 prathamdby + reeltimeapps RLTCmpe Rolf Fredheim rubyrunsstuff Samrat Jha snopoke wes-davis wstock YuriNachos Zach Knickerbocker + zknicker Azade ddyo Erik latitudeki5223 Manuel Maly Mourad Boustani pcty-nextgen-ios-builder Quentin Randy Torres + William Stock

diff --git a/scripts/clawtributors-map.json b/scripts/clawtributors-map.json index 5d75a5e8a..5327ebbff 100644 --- a/scripts/clawtributors-map.json +++ b/scripts/clawtributors-map.json @@ -19,6 +19,7 @@ }, "emailToLogin": { "steipete@gmail.com": "steipete", + "sbarrios93@gmail.com": "sebslight", "rltorres26+github@gmail.com": "RandyVentures", "hixvac@gmail.com": "VACInc" } diff --git a/src/agents/pi-embedded-helpers.test.ts b/src/agents/pi-embedded-helpers.test.ts index a9f63fc9f..ec7b0a662 100644 --- a/src/agents/pi-embedded-helpers.test.ts +++ b/src/agents/pi-embedded-helpers.test.ts @@ -363,6 +363,37 @@ describe("sanitizeSessionMessagesImages", () => { expect((content as Array<{ type?: string }>)[0]?.type).toBe("toolCall"); }); + it("sanitizes tool ids for assistant blocks and tool results", async () => { + const input = [ + { + role: "assistant", + content: [ + { type: "toolUse", id: "call_abc|item:123", name: "test", input: {} }, + { + type: "toolCall", + id: "call_abc|item:456", + name: "bash", + arguments: {}, + }, + ], + }, + { + role: "toolResult", + toolUseId: "call_abc|item:123", + content: [{ type: "text", text: "ok" }], + }, + ] satisfies AgentMessage[]; + + const out = await sanitizeSessionMessagesImages(input, "test"); + + const assistant = out[0] as { content?: Array<{ id?: string }> }; + expect(assistant.content?.[0]?.id).toBe("call_abc_item_123"); + expect(assistant.content?.[1]?.id).toBe("call_abc_item_456"); + + const toolResult = out[1] as { toolUseId?: string }; + expect(toolResult.toolUseId).toBe("call_abc_item_123"); + }); + it("filters whitespace-only assistant text blocks", async () => { const input = [ { diff --git a/src/agents/pi-embedded-helpers.ts b/src/agents/pi-embedded-helpers.ts index 049647ac5..61f0b5798 100644 --- a/src/agents/pi-embedded-helpers.ts +++ b/src/agents/pi-embedded-helpers.ts @@ -106,12 +106,20 @@ export async function sanitizeSessionMessagesImages( const sanitizedToolCallId = toolMsg.toolCallId ? sanitizeToolCallId(toolMsg.toolCallId) : undefined; + const toolUseId = (toolMsg as { toolUseId?: unknown }).toolUseId; + const sanitizedToolUseId = + typeof toolUseId === "string" && toolUseId + ? sanitizeToolCallId(toolUseId) + : undefined; const sanitizedMsg = { ...toolMsg, content: nextContent, ...(sanitizedToolCallId && { toolCallId: sanitizedToolCallId, }), + ...(sanitizedToolUseId && { + toolUseId: sanitizedToolUseId, + }), }; out.push(sanitizedMsg); continue; @@ -153,9 +161,13 @@ export async function sanitizeSessionMessagesImages( if (typeof id !== "string" || !id) return block; // Cloud Code Assist tool blocks require ids matching ^[a-zA-Z0-9_-]+$. - if (type === "functionCall" || type === "toolUse" || type === "toolCall") { + if ( + type === "functionCall" || + type === "toolUse" || + type === "toolCall" + ) { return { - ...((block as unknown) as Record), + ...(block as unknown as Record), id: sanitizeToolCallId(id), }; } diff --git a/src/agents/pi-tools.test.ts b/src/agents/pi-tools.test.ts index a4eabc81f..d89e2051b 100644 --- a/src/agents/pi-tools.test.ts +++ b/src/agents/pi-tools.test.ts @@ -385,6 +385,18 @@ describe("createClawdbotCodingTools", () => { "$defs", "definitions", "examples", + "minLength", + "maxLength", + "minimum", + "maximum", + "multipleOf", + "pattern", + "format", + "minItems", + "maxItems", + "uniqueItems", + "minProperties", + "maxProperties", ]); const findUnsupportedKeywords = ( @@ -399,9 +411,24 @@ describe("createClawdbotCodingTools", () => { }); return found; } - for (const [key, value] of Object.entries( - schema as Record, - )) { + + const record = schema as Record; + const properties = + record.properties && + typeof record.properties === "object" && + !Array.isArray(record.properties) + ? (record.properties as Record) + : undefined; + if (properties) { + for (const [key, value] of Object.entries(properties)) { + found.push( + ...findUnsupportedKeywords(value, `${path}.properties.${key}`), + ); + } + } + + for (const [key, value] of Object.entries(record)) { + if (key === "properties") continue; if (unsupportedKeywords.has(key)) { found.push(`${path}.${key}`); }