From 0d98e93253f2e1e98d3a345e8cc51b52e8b32db8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 10 Jan 2026 18:03:43 +0100 Subject: [PATCH] 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}`); }