fix(ai): skip AJV validation in restricted runtimes closes #2395
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
### Fixed
|
### 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 `<authenticated>` and fall back to ADC instead of sending them as literal API keys ([#2335](https://github.com/badlogic/pi-mono/issues/2335))
|
- Fixed `google-vertex` API key resolution to ignore placeholder auth markers like `<authenticated>` 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
|
## [0.60.0] - 2026-03-18
|
||||||
|
|||||||
@@ -11,10 +11,22 @@ import type { Tool, ToolCall } from "../types.js";
|
|||||||
// Chrome extensions with Manifest V3 don't allow eval/Function constructor
|
// Chrome extensions with Manifest V3 don't allow eval/Function constructor
|
||||||
const isBrowserExtension = typeof globalThis !== "undefined" && (globalThis as any).chrome?.runtime?.id !== undefined;
|
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)
|
function canUseRuntimeCodegen(): boolean {
|
||||||
// AJV requires 'unsafe-eval' CSP which is not allowed in Manifest V3
|
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;
|
let ajv: any = null;
|
||||||
if (!isBrowserExtension) {
|
if (canUseRuntimeCodegen()) {
|
||||||
try {
|
try {
|
||||||
ajv = new Ajv({
|
ajv = new Ajv({
|
||||||
allErrors: true,
|
allErrors: true,
|
||||||
@@ -23,7 +35,6 @@ if (!isBrowserExtension) {
|
|||||||
});
|
});
|
||||||
addFormats(ajv);
|
addFormats(ajv);
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
// AJV initialization failed (likely CSP restriction)
|
|
||||||
console.warn("AJV validation disabled due to CSP restrictions");
|
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
|
* @throws Error with formatted message if validation fails
|
||||||
*/
|
*/
|
||||||
export function validateToolArguments(tool: Tool, toolCall: ToolCall): any {
|
export function validateToolArguments(tool: Tool, toolCall: ToolCall): any {
|
||||||
// Skip validation in browser extension environment (CSP restrictions prevent AJV from working)
|
// Skip validation in environments where runtime code generation is unavailable.
|
||||||
if (!ajv || isBrowserExtension) {
|
if (!ajv || !canUseRuntimeCodegen()) {
|
||||||
// Trust the LLM's output without validation
|
|
||||||
// Browser extensions can't use AJV due to Manifest V3 CSP restrictions
|
|
||||||
return toolCall.arguments;
|
return toolCall.arguments;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile the schema
|
// Compile the schema.
|
||||||
const validate = ajv.compile(tool.parameters);
|
const validate = ajv.compile(tool.parameters);
|
||||||
|
|
||||||
// Clone arguments so AJV can safely mutate for type coercion
|
// Clone arguments so AJV can safely mutate for type coercion
|
||||||
|
|||||||
39
packages/ai/test/validation.test.ts
Normal file
39
packages/ai/test/validation.test.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user