fix(agents): guard tool definition schemas

This commit is contained in:
Vincent Koc
2026-06-04 10:51:45 +02:00
parent f875b519e5
commit f34401db48
2 changed files with 63 additions and 2 deletions

View File

@@ -43,6 +43,24 @@ async function executeTool(tool: AgentTool, callId: string) {
}
describe("agent tool definition adapter", () => {
it("uses an empty object schema when an agent tool parameter getter throws", () => {
const tool = {
name: "hostile_schema",
label: "Hostile Schema",
description: "throws while reading parameters",
execute: async () => ({ content: [] }),
} as unknown as AgentTool;
Object.defineProperty(tool, "parameters", {
get() {
throw new Error("schema unavailable");
},
});
const [def] = toToolDefinitions([tool]);
expect(def?.parameters).toEqual({ type: "object", properties: {} });
});
it("wraps tool errors into a tool result", async () => {
const result = await executeThrowingTool("boom", "call1");
@@ -140,6 +158,22 @@ async function executeClientTool(params: unknown): Promise<{
}
describe("toClientToolDefinitions param coercion", () => {
it("uses an empty object schema when a client tool parameter getter throws", () => {
const func = {
name: "client_schema",
description: "throws while reading parameters",
} as ClientToolDefinition["function"];
Object.defineProperty(func, "parameters", {
get() {
throw new Error("schema unavailable");
},
});
const [def] = toClientToolDefinitions([{ type: "function", function: func }]);
expect(def?.parameters).toEqual({ type: "object", properties: {} });
});
it("returns terminal pending results for each client tool in a batch", async () => {
const completed: Array<{ id: string; name: string; params: Record<string, unknown> }> = [];
const defs = toClientToolDefinitions([makeClientTool("search"), makeClientTool("lookup")], {

View File

@@ -53,6 +53,7 @@ const TOOL_ERROR_PARAM_PREVIEW_MAX_CHARS = 600;
const TOOL_ERROR_EXEC_COMMAND_HASH_CHARS = 16;
const SENSITIVE_EXEC_ENV_VALUE = "[omitted exec env value]";
const EXEC_COMMAND_PARAM_KEYS = new Set(["command", "cmd"]);
const EMPTY_OBJECT_TOOL_PARAMETERS = { type: "object", properties: {} } as ToolDefinition["parameters"];
export type ClientToolCallRecorder =
| ((toolName: string, params: Record<string, unknown>) => void)
@@ -135,6 +136,32 @@ function kindForLog(value: unknown): string {
return typeof value;
}
function resolveAgentToolParameters(tool: AnyAgentTool): ToolDefinition["parameters"] {
try {
return tool.parameters;
} catch (err) {
const described = describeToolExecutionError(err);
logDebug(
`tools: ${tool.name || "tool"} has unreadable parameter schema; using empty object schema: ${described.message}`,
);
return EMPTY_OBJECT_TOOL_PARAMETERS;
}
}
function resolveClientToolParameters(
func: ClientToolDefinition["function"],
): ToolDefinition["parameters"] {
try {
return func.parameters as ToolDefinition["parameters"];
} catch (err) {
const described = describeToolExecutionError(err);
logDebug(
`tools: ${func.name || "client_tool"} has unreadable client parameter schema; using empty object schema: ${described.message}`,
);
return EMPTY_OBJECT_TOOL_PARAMETERS;
}
}
function summarizeSensitiveValueForLog(params: {
value: unknown;
reason: string;
@@ -358,7 +385,7 @@ export function toToolDefinitions(
name,
label: tool.label ?? name,
description: tool.description ?? "",
parameters: tool.parameters,
parameters: resolveAgentToolParameters(tool),
execute: async (...args: ToolExecuteArgs): Promise<AgentToolResult<unknown>> => {
const { toolCallId, params, onUpdate, signal } = splitToolExecuteArgs(args);
let executeParams = params;
@@ -490,7 +517,7 @@ export function toClientToolDefinitions(
name: func.name,
label: func.name,
description: func.description ?? "",
parameters: func.parameters as ToolDefinition["parameters"],
parameters: resolveClientToolParameters(func),
execute: async (...args: ToolExecuteArgs): Promise<AgentToolResult<unknown>> => {
const { toolCallId, params } = splitToolExecuteArgs(args);
if (onClientToolCall && typeof onClientToolCall !== "function") {