diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 837c8357..1ea170c7 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Fixed `validateToolArguments()` to fall back gracefully when AJV schema compilation is blocked in restricted runtimes such as Cloudflare Workers, allowing tool execution to proceed without schema validation ([#2395](https://github.com/badlogic/pi-mono/issues/2395)) - Fixed `google-vertex` API key resolution to ignore placeholder auth markers like `` and fall back to ADC instead of sending them as literal API keys ([#2335](https://github.com/badlogic/pi-mono/issues/2335)) ## [0.60.0] - 2026-03-18 diff --git a/packages/ai/src/utils/validation.ts b/packages/ai/src/utils/validation.ts index b5f48faa..3a54fa13 100644 --- a/packages/ai/src/utils/validation.ts +++ b/packages/ai/src/utils/validation.ts @@ -11,10 +11,22 @@ import type { Tool, ToolCall } from "../types.js"; // Chrome extensions with Manifest V3 don't allow eval/Function constructor const isBrowserExtension = typeof globalThis !== "undefined" && (globalThis as any).chrome?.runtime?.id !== undefined; -// Create a singleton AJV instance with formats (only if not in browser extension) -// AJV requires 'unsafe-eval' CSP which is not allowed in Manifest V3 +function canUseRuntimeCodegen(): boolean { + if (isBrowserExtension) { + return false; + } + + try { + new Function("return true;"); + return true; + } catch { + return false; + } +} + +// Create a singleton AJV instance with formats only when runtime code generation is available. let ajv: any = null; -if (!isBrowserExtension) { +if (canUseRuntimeCodegen()) { try { ajv = new Ajv({ allErrors: true, @@ -23,7 +35,6 @@ if (!isBrowserExtension) { }); addFormats(ajv); } catch (_e) { - // AJV initialization failed (likely CSP restriction) console.warn("AJV validation disabled due to CSP restrictions"); } } @@ -51,14 +62,12 @@ export function validateToolCall(tools: Tool[], toolCall: ToolCall): any { * @throws Error with formatted message if validation fails */ export function validateToolArguments(tool: Tool, toolCall: ToolCall): any { - // Skip validation in browser extension environment (CSP restrictions prevent AJV from working) - if (!ajv || isBrowserExtension) { - // Trust the LLM's output without validation - // Browser extensions can't use AJV due to Manifest V3 CSP restrictions + // Skip validation in environments where runtime code generation is unavailable. + if (!ajv || !canUseRuntimeCodegen()) { return toolCall.arguments; } - // Compile the schema + // Compile the schema. const validate = ajv.compile(tool.parameters); // Clone arguments so AJV can safely mutate for type coercion diff --git a/packages/ai/test/validation.test.ts b/packages/ai/test/validation.test.ts new file mode 100644 index 00000000..60de63a6 --- /dev/null +++ b/packages/ai/test/validation.test.ts @@ -0,0 +1,39 @@ +import { Type } from "@sinclair/typebox"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { ToolCall } from "../src/types.js"; +import { validateToolArguments } from "../src/utils/validation.js"; + +afterEach(() => { + vi.restoreAllMocks(); +}); + +describe("validateToolArguments", () => { + it("falls back to raw arguments without writing to stderr when runtime code generation is blocked", () => { + const originalFunction = globalThis.Function; + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + const tool = { + name: "echo", + description: "Echo tool", + parameters: Type.Object({ + count: Type.Number(), + }), + }; + const toolCall: ToolCall = { + type: "toolCall", + id: "tool-1", + name: "echo", + arguments: { count: "42" as unknown as number }, + }; + + globalThis.Function = (() => { + throw new EvalError("Code generation from strings disallowed for this context"); + }) as unknown as FunctionConstructor; + + try { + expect(validateToolArguments(tool, toolCall)).toEqual(toolCall.arguments); + expect(errorSpy).not.toHaveBeenCalled(); + } finally { + globalThis.Function = originalFunction; + } + }); +});