fix(agents): bound runtime tool list projection

This commit is contained in:
Vincent Koc
2026-06-04 08:17:51 +02:00
parent 1053a76dd8
commit b8d9e0987b
2 changed files with 48 additions and 1 deletions

View File

@@ -134,6 +134,32 @@ describe("runtime tool input schema projection", () => {
});
});
it("reports invalid runtime tool list lengths", () => {
const healthy = {
name: "healthy",
parameters: { type: "object", properties: {} },
};
const proxy = new Proxy([healthy] as Array<typeof healthy>, {
get(target, property, receiver) {
if (property === "length") {
return -1;
}
return Reflect.get(target, property, receiver);
},
});
expect(filterRuntimeCompatibleTools(proxy)).toEqual({
tools: [],
diagnostics: [
{
toolName: "tool[0]",
toolIndex: 0,
violations: ["runtime tool list length is invalid"],
},
],
});
});
it("quarantines unreadable runtime tool fields without dropping healthy siblings", () => {
const unreadable = {
name: "fuzzplugin_unreadable",

View File

@@ -41,15 +41,18 @@ type RuntimeToolEntryRead<TTool extends Pick<AnyAgentTool, "name" | "parameters"
type ToolSchemaInspectionMode = "runtime" | "provider-normalizable";
const MAX_RUNTIME_TOOL_ENTRY_READS = 10_000;
function unreadableRuntimeToolEntry(
toolIndex: number,
violation = `tool[${toolIndex}] is unreadable`,
): RuntimeToolEntryRead<Pick<AnyAgentTool, "name" | "parameters">> {
return {
ok: false,
diagnostic: {
toolName: `tool[${toolIndex}]`,
toolIndex,
violations: [`tool[${toolIndex}] is unreadable`],
violations: [violation],
},
};
}
@@ -63,6 +66,24 @@ function readRuntimeToolEntries<TTool extends Pick<AnyAgentTool, "name" | "param
} catch {
return [unreadableRuntimeToolEntry(0) as RuntimeToolEntryRead<TTool>];
}
if (!Number.isSafeInteger(length) || length < 0) {
return [
unreadableRuntimeToolEntry(
0,
"runtime tool list length is invalid",
) as RuntimeToolEntryRead<TTool>,
];
}
// Projection is a safety check for plugin-controlled tool lists. Reject
// hostile array-like lengths before diagnostics become an unbounded loop.
if (length > MAX_RUNTIME_TOOL_ENTRY_READS) {
return [
unreadableRuntimeToolEntry(
MAX_RUNTIME_TOOL_ENTRY_READS,
`runtime tool list length exceeds ${MAX_RUNTIME_TOOL_ENTRY_READS}`,
) as RuntimeToolEntryRead<TTool>,
];
}
const entries: RuntimeToolEntryRead<TTool>[] = [];
for (let toolIndex = 0; toolIndex < length; toolIndex += 1) {
try {