diff --git a/CHANGELOG.md b/CHANGELOG.md
index 43ef36ff3..e19554671 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,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:
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
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 cae4ba51f..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;
@@ -146,18 +154,24 @@ 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") 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 (
- block &&
- typeof block === "object" &&
- (block as { type?: unknown }).type === "functionCall" &&
- (block as { id?: unknown }).id
+ type === "functionCall" ||
+ type === "toolUse" ||
+ type === "toolCall"
) {
- const functionBlock = block as { type: string; id: string };
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..d89e2051b 100644
--- a/src/agents/pi-tools.test.ts
+++ b/src/agents/pi-tools.test.ts
@@ -384,6 +384,19 @@ describe("createClawdbotCodingTools", () => {
"$ref",
"$defs",
"definitions",
+ "examples",
+ "minLength",
+ "maxLength",
+ "minimum",
+ "maximum",
+ "multipleOf",
+ "pattern",
+ "format",
+ "minItems",
+ "maxItems",
+ "uniqueItems",
+ "minProperties",
+ "maxProperties",
]);
const findUnsupportedKeywords = (
@@ -398,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}`);
}
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;