diff --git a/docs/error-contract.schema.json b/docs/error-contract.schema.json index c1286e5d05..1d5f875cbc 100644 --- a/docs/error-contract.schema.json +++ b/docs/error-contract.schema.json @@ -29,7 +29,7 @@ "details": { "type": "object", "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } + "additionalProperties": {} } }, "required": ["code", "category", "message", "retryable", "remediation"], @@ -63,7 +63,7 @@ "details": { "type": "object", "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } + "additionalProperties": {} } }, "required": ["code", "category", "message", "retryable", "remediation"], @@ -97,7 +97,7 @@ "details": { "type": "object", "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } + "additionalProperties": {} } }, "required": ["code", "category", "message", "retryable", "remediation"], @@ -131,7 +131,7 @@ "details": { "type": "object", "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } + "additionalProperties": {} } }, "required": ["code", "category", "message", "retryable", "remediation"], @@ -165,7 +165,7 @@ "details": { "type": "object", "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } + "additionalProperties": {} } }, "required": ["code", "category", "message", "retryable", "remediation"], @@ -199,7 +199,7 @@ "details": { "type": "object", "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } + "additionalProperties": {} } }, "required": ["code", "category", "message", "retryable", "remediation"], @@ -233,7 +233,7 @@ "details": { "type": "object", "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } + "additionalProperties": {} } }, "required": ["code", "category", "message", "retryable", "remediation"], @@ -267,7 +267,7 @@ "details": { "type": "object", "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } + "additionalProperties": {} } }, "required": ["code", "category", "message", "retryable", "remediation"], @@ -357,7 +357,7 @@ "details": { "type": "object", "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } + "additionalProperties": {} } }, "required": ["code", "category", "message", "retryable", "remediation"], @@ -391,7 +391,7 @@ "details": { "type": "object", "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } + "additionalProperties": {} } }, "required": ["code", "category", "message", "retryable", "remediation"], @@ -425,7 +425,7 @@ "details": { "type": "object", "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } + "additionalProperties": {} } }, "required": ["code", "category", "message", "retryable", "remediation"], @@ -459,7 +459,7 @@ "details": { "type": "object", "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } + "additionalProperties": {} } }, "required": ["code", "category", "message", "retryable", "remediation"], @@ -493,7 +493,7 @@ "details": { "type": "object", "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } + "additionalProperties": {} } }, "required": ["code", "category", "message", "retryable", "remediation"], @@ -527,7 +527,7 @@ "details": { "type": "object", "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } + "additionalProperties": {} } }, "required": ["code", "category", "message", "retryable", "remediation"], @@ -561,7 +561,7 @@ "details": { "type": "object", "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } + "additionalProperties": {} } }, "required": ["code", "category", "message", "retryable", "remediation"], @@ -595,7 +595,7 @@ "details": { "type": "object", "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } + "additionalProperties": {} } }, "required": ["code", "category", "message", "retryable", "remediation"], @@ -629,27 +629,11 @@ "details": { "type": "object", "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } + "additionalProperties": {} } }, "required": ["code", "category", "message", "retryable", "remediation"], "additionalProperties": false } - ], - "$defs": { - "__schema0": { - "anyOf": [ - { "type": "string" }, - { "type": "number" }, - { "type": "boolean" }, - { "type": "null" }, - { "type": "array", "items": { "$ref": "#/$defs/__schema0" } }, - { - "type": "object", - "propertyNames": { "type": "string" }, - "additionalProperties": { "$ref": "#/$defs/__schema0" } - } - ] - } - } + ] } diff --git a/docs/product-catalog.json b/docs/product-catalog.json index 4da83b0a4a..d0dd89d347 100644 --- a/docs/product-catalog.json +++ b/docs/product-catalog.json @@ -689,9 +689,9 @@ "mcp_prompts": 6 }, "digests": { - "tools_sha256": "d6f2493e9ad693a762f1bd3daf9b6736571f2a190b2a41986e045bf6ff48af89", + "tools_sha256": "eb4749bb14998e23991c77e2d07e7cc69b28d98f2323f7fc3bbe6b4c2da79611", "prompts_sha256": "ef2de8814390b340623c32001610aa2709d90bed51537b6e562ab3abf144ab4d", - "combined_sha256": "e41a84b460b65f3a8d0d33498d958d249a1de81a9660c18580a0e3ab5d6941e1", + "combined_sha256": "0417b6ac9d07ff805aa6ec17f58577fc341320d46da50137da73b1c6dd0d3947", "providers_sha256": "e601cdceb5981ee04d6f431492f961f8867b1faa4f08ca0ecd29be2a239bc467" } } diff --git a/docs/verification/managed-conformance-manifest.json b/docs/verification/managed-conformance-manifest.json index 78be548687..a0905ad824 100644 --- a/docs/verification/managed-conformance-manifest.json +++ b/docs/verification/managed-conformance-manifest.json @@ -1,5 +1,5 @@ { - "manifest_id": "ecm_0615df0eb6f041eb2d145edbca9c4068a7dc9cbe53801dd237c7dd9394a2fa40", + "manifest_id": "ecm_6c5378cbeb0a0d0c0b127b1961f2b5e50d6541601a5f36ad7f5fb292e17ff203", "verifier": { "id": "rea-managed-conformance", "version": "1" @@ -13,7 +13,7 @@ "skill_digests": [ { "skill_id": "reverse-engineer-anything", - "sha256": "49845719d9ea2d229fb954ba92203921e843908b2f116c94ecb94636812edc42" + "sha256": "7777298dede63ed91211af502a4779850bfe451a11758349150d5a3df4261a93" } ], "claims": [ diff --git a/skills/reverse-engineer-anything/SKILL.md b/skills/reverse-engineer-anything/SKILL.md index 54ed16b342..9cf844f464 100644 --- a/skills/reverse-engineer-anything/SKILL.md +++ b/skills/reverse-engineer-anything/SKILL.md @@ -4,7 +4,7 @@ description: Reverse engineer native, managed, Electron/JavaScript, packaged, fi metadata: version: "28" tool_count: 134 - catalog_digest: "e41a84b460b65f3a8d0d33498d958d249a1de81a9660c18580a0e3ab5d6941e1" + catalog_digest: "0417b6ac9d07ff805aa6ec17f58577fc341320d46da50137da73b1c6dd0d3947" --- # REA diff --git a/src/contracts/errorSchemas.ts b/src/contracts/errorSchemas.ts index d5c0e78acb..3ea7f5c76e 100644 --- a/src/contracts/errorSchemas.ts +++ b/src/contracts/errorSchemas.ts @@ -1,5 +1,7 @@ import { z } from "zod"; +import { jsonValueSchema } from "../domain/jsonValue.js"; + const categorySchema = z.enum([ "invalid_input", "unsupported_provider", @@ -21,7 +23,7 @@ const common = { retryable: z.boolean(), remediation: remediationSchema, }; -const genericDetails = z.record(z.string(), z.json()).optional(); +const genericDetails = z.record(z.string(), jsonValueSchema).optional(); const generic = (code: Code) => z .object({ code: z.literal(code), ...common, details: genericDetails }) diff --git a/src/contracts/toolOutputSchemaPrimitives.ts b/src/contracts/toolOutputSchemaPrimitives.ts index bf4a4e25e9..9e064c2c3b 100644 --- a/src/contracts/toolOutputSchemaPrimitives.ts +++ b/src/contracts/toolOutputSchemaPrimitives.ts @@ -363,7 +363,7 @@ export const sessionProvider = z negotiated_protocol_version: z.string().nullable(), client: z.object({ name: z.string(), version: z.string() }).nullable(), skill: z.object({ name: z.string(), expected_version: z.string() }), - catalog: z.record(z.string(), z.json()), + catalog: z.record(z.string(), jsonValueSchema), protocol_features: z.object({ progress: z.boolean(), cancellation: z.boolean(), diff --git a/src/domain/jsonValue.test.ts b/src/domain/jsonValue.test.ts new file mode 100644 index 0000000000..217c44626d --- /dev/null +++ b/src/domain/jsonValue.test.ts @@ -0,0 +1,37 @@ +import { Ajv2020 } from "ajv/dist/2020.js"; +import { expect, it } from "vitest"; +import { z } from "zod"; + +import { jsonObjectSchema, jsonValueSchema } from "./jsonValue.js"; + +const values = [ + "text", + 0, + false, + null, + [], + {}, + { nested: [{ deeper: [1, "two", null, { three: true }] }] }, +]; + +it("projects JSON values without recursive definitions", () => { + for (const io of ["input", "output"] as const) { + const schema = z.toJSONSchema( + z.object({ value: jsonValueSchema, map: jsonObjectSchema }), + { io }, + ); + expect(JSON.stringify(schema)).not.toContain("$ref"); + expect(schema).not.toHaveProperty("$defs"); + const validate = new Ajv2020({ strict: false }).compile(schema); + for (const value of values) + expect(validate({ value, map: { value } })).toBe(true); + expect(validate({ value: 1, map: [] })).toBe(false); + } +}); + +it("still parses only JSON values at runtime", () => { + for (const value of values) + expect(jsonValueSchema.safeParse(value).success).toBe(true); + for (const value of [undefined, Number.NaN, Infinity, () => 0, [undefined]]) + expect(jsonValueSchema.safeParse(value).success).toBe(false); +}); diff --git a/src/domain/jsonValue.ts b/src/domain/jsonValue.ts index 710e62d612..1917eac31d 100644 --- a/src/domain/jsonValue.ts +++ b/src/domain/jsonValue.ts @@ -2,6 +2,10 @@ import { z } from "zod"; /** JSON-safe value shared by domain, application, and adapter boundaries. */ export const jsonValueSchema = z.json(); +// Every JSON value satisfies `{}`. Advertise that instead of Zod's +// self-referential `$defs` projection, which MCP clients and model APIs that +// reject recursive JSON Schemas refuse. Runtime parsing still admits only JSON. +jsonValueSchema._zod.toJSONSchema = () => ({}); /** JSON object boundary for caller-visible parameter maps. */ export const jsonObjectSchema = z.record(z.string(), jsonValueSchema); diff --git a/tests/boundary/mcp/toolSchemaValidity.test.ts b/tests/boundary/mcp/toolSchemaValidity.test.ts index fb72e8f95a..708f8d40dc 100644 --- a/tests/boundary/mcp/toolSchemaValidity.test.ts +++ b/tests/boundary/mcp/toolSchemaValidity.test.ts @@ -129,6 +129,55 @@ function expectRecursivePropertyDescriptions( const isRecord = (value: unknown): value is Record => typeof value === "object" && value !== null && !Array.isArray(value); +function resolveReference(schema: unknown, reference: string): unknown { + if (reference === "#") return schema; + if (!reference.startsWith("#/")) return undefined; + let value = schema; + for (const token of reference.slice(2).split("/")) { + const key = token.replaceAll("~1", "/").replaceAll("~0", "~"); + if ( + typeof value !== "object" || + value === null || + !Object.hasOwn(value, key) + ) + return undefined; + value = Reflect.get(value, key); + } + return value; +} + +function recursiveReferences(tools: readonly ToolSchemas[]): string[] { + return tools.flatMap((tool) => + (["inputSchema", "outputSchema"] as const).flatMap((kind) => { + const schema = tool[kind]; + const found = new Set(); + const visit = (node: unknown, active: readonly string[]): void => { + if (typeof node !== "object" || node === null) return; + for (const [key, child] of Object.entries(node)) { + if ( + [ + "$defs", + "definitions", + "examples", + "default", + "const", + "enum", + ].includes(key) + ) + continue; + if (key !== "$ref" || typeof child !== "string") visit(child, active); + else if (active.includes(child)) found.add(child); + else visit(resolveReference(schema, child), [...active, child]); + } + }; + visit(schema, []); + return [...found].map( + (reference) => `${tool.name}.${kind}: ${reference}`, + ); + }), + ); +} + const advertiseAndEnforceProcessEnvironmentKeyConstraint = async (): Promise => { const contract = TOOL_CONTRACTS.find( @@ -240,6 +289,7 @@ describe("MCP JSON Schema validity", () => { TOOL_CONTRACTS.map(({ name }) => name).sort(), ); expect(schemaErrors(tools)).toEqual([]); + expect(recursiveReferences(tools)).toEqual([]); expectKnownAuthorityHints(tools); const byName = new Map(tools.map((tool) => [tool.name, tool])); const catalogProjection = tools.map((tool) => ({ @@ -414,3 +464,32 @@ describe("MCP process input JSON Schema", () => { } }); }); + +it("ships nonrecursive input and output schemas in the generated catalog", () => { + expect(schemaErrors(GENERATED_MCP_TOOL_CATALOG)).toEqual([]); + expect(recursiveReferences(GENERATED_MCP_TOOL_CATALOG)).toEqual([]); +}); + +it("distinguishes recursive references from shared definitions", () => { + const diamond = { + type: "object", + properties: { + left: { $ref: "#/$defs/shared" }, + right: { $ref: "#/$defs/shared" }, + }, + $defs: { shared: { type: "string" } }, + }; + expect( + recursiveReferences([{ name: "diamond", inputSchema: diamond }]), + ).toEqual([]); + expect( + recursiveReferences([{ name: "root", inputSchema: { $ref: "#" } }]), + ).toEqual(["root.inputSchema: #"]); + const escaped = { + $ref: "#/$defs/a~1b", + $defs: { "a/b": { $ref: "#/$defs/a~1b" } }, + }; + expect( + recursiveReferences([{ name: "escaped", inputSchema: escaped }]), + ).toEqual(["escaped.inputSchema: #/$defs/a~1b"]); +});