From a33e4a5cc0f1d82f9b4c703cc7a2dbc7269ba895 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 30 Jun 2026 06:21:05 +0900 Subject: [PATCH] fix(kiro): strip validation-only schema keywords rejected by runtimeservice codex-rs memories__* tools (add_ad_hoc_note/read/search/list) emit JSON Schema validation keywords (pattern, minLength/maxLength, minimum, minItems) via schemars. Kiro's runtimeservice tool-spec validator rejects these with "ValidationException: Invalid tool use format", breaking every memory tool call. sanitizeKiroSchema now drops the full set of validation/applicator keywords Bedrock/Kiro do not support (pattern, format, length/range bounds, patternProperties, propertyNames, if/then/else, etc.) while preserving structural keywords (type, properties, items, enum, required, $ref/$defs). Property and definition name maps are recursed into without treating their child names as keywords, so a property literally named "format"/"pattern" survives. --- src/adapters/kiro-tools.ts | 57 +++++++++++++++++++++++++++++++++-- tests/kiro-adapter.test.ts | 61 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 2 deletions(-) diff --git a/src/adapters/kiro-tools.ts b/src/adapters/kiro-tools.ts index 0b825e4b40c..2811f99cf1c 100644 --- a/src/adapters/kiro-tools.ts +++ b/src/adapters/kiro-tools.ts @@ -3,14 +3,67 @@ import { namespacedToolName } from "../types"; const MAX_KIRO_TOOL_DESCRIPTION = 1024; +// JSON Schema validation/annotation keywords that Kiro's runtimeservice tool-spec validator +// rejects ("ValidationException: Invalid tool use format."). Codex's built-in tools omit these, +// but the `memories__*` tools (add_ad_hoc_note/read/search/list) emit pattern/length/range +// constraints via schemars, which trip the validator. Strip them everywhere in the schema tree; +// the constraints are advisory for the model, so dropping them does not change tool behavior. +const KIRO_REJECTED_SCHEMA_KEYS = new Set([ + "additionalProperties", + "pattern", + "format", + "minLength", + "maxLength", + "minimum", + "maximum", + "exclusiveMinimum", + "exclusiveMaximum", + "multipleOf", + "minItems", + "maxItems", + "uniqueItems", + "minProperties", + "maxProperties", + "contentEncoding", + "contentMediaType", + "$schema", + // Validation-only composition/applicator keywords that Bedrock/Kiro do not support. Unlike + // `properties`/`$defs`, these are not plain property->schema maps the model needs, so they are + // dropped outright rather than recursed into. + "patternProperties", + "propertyNames", + "dependentSchemas", + "dependentRequired", + "if", + "then", + "else", + "contains", + "unevaluatedProperties", + "unevaluatedItems", +]); + +// Keys whose values are maps of *property/definition name -> schema* (not schema keywords). Their +// child keys must never be treated as schema keywords, or a legitimate property named e.g. +// "format"/"pattern" would be deleted. We recurse into the value schemas but keep every name intact. +const SCHEMA_MAP_KEYS = new Set(["properties", "$defs", "definitions"]); + +function sanitizeSchemaMap(value: unknown): unknown { + if (!value || typeof value !== "object" || Array.isArray(value)) return sanitizeKiroSchema(value); + const out: Record = {}; + for (const [name, child] of Object.entries(value as Record)) { + out[name] = sanitizeKiroSchema(child); + } + return out; +} + function sanitizeKiroSchema(value: unknown): unknown { if (Array.isArray(value)) return value.map(sanitizeKiroSchema); if (!value || typeof value !== "object") return value; const out: Record = {}; for (const [key, child] of Object.entries(value as Record)) { - if (key === "additionalProperties") continue; + if (KIRO_REJECTED_SCHEMA_KEYS.has(key)) continue; if (key === "required" && Array.isArray(child) && child.length === 0) continue; - out[key] = sanitizeKiroSchema(child); + out[key] = SCHEMA_MAP_KEYS.has(key) ? sanitizeSchemaMap(child) : sanitizeKiroSchema(child); } return out; } diff --git a/tests/kiro-adapter.test.ts b/tests/kiro-adapter.test.ts index a39f51858e6..944bfbf433d 100644 --- a/tests/kiro-adapter.test.ts +++ b/tests/kiro-adapter.test.ts @@ -233,6 +233,67 @@ describe("kiro adapter — buildRequest", () => { expect(schema.properties.options.additionalProperties).toBeUndefined(); }); + test("memory-style validation constraints are stripped but property names are preserved", () => { + // Mirrors codex-rs memories tools (add_ad_hoc_note/read/search): schemars emits + // pattern/length/range keywords that Kiro's runtimeservice rejects as "Invalid tool use format". + const parameters = { + type: "object", + properties: { + filename: { type: "string", pattern: "^\\d{4}.*\\.md$", minLength: 24, maxLength: 128 }, + note: { type: "string", minLength: 1 }, + max_lines: { type: "integer", minimum: 1 }, + queries: { type: "array", items: { type: "string" }, minItems: 1 }, + // A property literally named "pattern"/"format" must survive untouched. + pattern: { type: "string", format: "uuid" }, + format: { type: "string" }, + }, + required: ["filename", "note"], + }; + const { body } = createKiroAdapter(provider).buildRequest( + parsedWith([{ role: "user", content: "hi" }], [{ name: "memories__add_ad_hoc_note", description: "Remember", parameters }]), + ); + const schema = JSON.parse(body).conversationState.currentMessage.userInputMessage.userInputMessageContext.tools[0].toolSpecification.inputSchema.json; + + expect(schema.properties.filename.pattern).toBeUndefined(); + expect(schema.properties.filename.minLength).toBeUndefined(); + expect(schema.properties.filename.maxLength).toBeUndefined(); + expect(schema.properties.filename.type).toBe("string"); + expect(schema.properties.note.minLength).toBeUndefined(); + expect(schema.properties.max_lines.minimum).toBeUndefined(); + expect(schema.properties.queries.minItems).toBeUndefined(); + expect(schema.properties.queries.items).toEqual({ type: "string" }); + // Property names that collide with schema keywords must be kept as properties. + expect(schema.properties.pattern).toBeDefined(); + expect(schema.properties.pattern.format).toBeUndefined(); + expect(schema.properties.format).toBeDefined(); + expect(schema.required).toEqual(["filename", "note"]); + }); + + test("validation-only applicator keywords are dropped while $defs are preserved", () => { + const parameters = { + type: "object", + properties: { + ref_field: { $ref: "#/$defs/Inner" }, + tags: { type: "object", patternProperties: { "^x-": { type: "string" } } }, + }, + patternProperties: { "^meta_": { type: "string", pattern: "^v" } }, + propertyNames: { pattern: "^[a-z]+$" }, + $defs: { Inner: { type: "object", properties: { id: { type: "string" } } } }, + }; + const { body } = createKiroAdapter(provider).buildRequest( + parsedWith([{ role: "user", content: "hi" }], [{ name: "memories__read", description: "Read", parameters }]), + ); + const schema = JSON.parse(body).conversationState.currentMessage.userInputMessage.userInputMessageContext.tools[0].toolSpecification.inputSchema.json; + + // Validation-only applicator keywords Bedrock/Kiro reject must be gone everywhere. + expect(schema.patternProperties).toBeUndefined(); + expect(schema.propertyNames).toBeUndefined(); + expect(schema.properties.tags.patternProperties).toBeUndefined(); + // $ref + $defs (real reuse, supported) survive, and the inner schema is sanitized too. + expect(schema.properties.ref_field).toEqual({ $ref: "#/$defs/Inner" }); + expect(schema.$defs.Inner.properties.id).toEqual({ type: "string" }); + }); + test("root inputSchema always declares type:object (Bedrock requires it)", () => { // Empty parameters (e.g. some MCP/Computer Use tools) must still surface type:"object" or // Bedrock rejects with "toolSpec.inputSchema.json.type must be one of the following: object".