mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 18:01:22 +00:00
* fix(ai): coerce string numbers in tool argument validation * fix(ai): clone tool arguments before AJV validation for type coercion
84 lines
2.9 KiB
TypeScript
84 lines
2.9 KiB
TypeScript
import AjvModule from "ajv";
|
|
import addFormatsModule from "ajv-formats";
|
|
|
|
// Handle both default and named exports
|
|
const Ajv = (AjvModule as any).default || AjvModule;
|
|
const addFormats = (addFormatsModule as any).default || addFormatsModule;
|
|
|
|
import type { Tool, ToolCall } from "../types.js";
|
|
|
|
// Detect if we're in a browser extension environment with strict CSP
|
|
// 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
|
|
let ajv: any = null;
|
|
if (!isBrowserExtension) {
|
|
try {
|
|
ajv = new Ajv({
|
|
allErrors: true,
|
|
strict: false,
|
|
coerceTypes: true,
|
|
});
|
|
addFormats(ajv);
|
|
} catch (_e) {
|
|
// AJV initialization failed (likely CSP restriction)
|
|
console.warn("AJV validation disabled due to CSP restrictions");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Finds a tool by name and validates the tool call arguments against its TypeBox schema
|
|
* @param tools Array of tool definitions
|
|
* @param toolCall The tool call from the LLM
|
|
* @returns The validated arguments
|
|
* @throws Error if tool is not found or validation fails
|
|
*/
|
|
export function validateToolCall(tools: Tool[], toolCall: ToolCall): any {
|
|
const tool = tools.find((t) => t.name === toolCall.name);
|
|
if (!tool) {
|
|
throw new Error(`Tool "${toolCall.name}" not found`);
|
|
}
|
|
return validateToolArguments(tool, toolCall);
|
|
}
|
|
|
|
/**
|
|
* Validates tool call arguments against the tool's TypeBox schema
|
|
* @param tool The tool definition with TypeBox schema
|
|
* @param toolCall The tool call from the LLM
|
|
* @returns The validated (and potentially coerced) arguments
|
|
* @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
|
|
return toolCall.arguments;
|
|
}
|
|
|
|
// Compile the schema
|
|
const validate = ajv.compile(tool.parameters);
|
|
|
|
// Clone arguments so AJV can safely mutate for type coercion
|
|
const args = structuredClone(toolCall.arguments);
|
|
|
|
// Validate the arguments (AJV mutates args in-place for type coercion)
|
|
if (validate(args)) {
|
|
return args;
|
|
}
|
|
|
|
// Format validation errors nicely
|
|
const errors =
|
|
validate.errors
|
|
?.map((err: any) => {
|
|
const path = err.instancePath ? err.instancePath.substring(1) : err.params.missingProperty || "root";
|
|
return ` - ${path}: ${err.message}`;
|
|
})
|
|
.join("\n") || "Unknown validation error";
|
|
|
|
const errorMessage = `Validation failed for tool "${toolCall.name}":\n${errors}\n\nReceived arguments:\n${JSON.stringify(toolCall.arguments, null, 2)}`;
|
|
|
|
throw new Error(errorMessage);
|
|
}
|