fix(plugins): skip unreadable tool descriptor cache entries

This commit is contained in:
Vincent Koc
2026-06-04 10:00:30 +02:00
parent 838644b989
commit c678a6ebc0
4 changed files with 96 additions and 23 deletions

View File

@@ -16,10 +16,38 @@ vi.mock("../config/runtime-snapshot.js", () => ({
import {
buildPluginToolDescriptorCacheKey,
capturePluginToolDescriptor,
createPluginToolDescriptorConfigCacheKeyMemo,
resetPluginToolDescriptorCache,
} from "./tool-descriptor-cache.js";
describe("plugin tool descriptor capture", () => {
it("skips unreadable plugin tool descriptor fields", () => {
const tool = {
name: "unstable_tool",
description: "unstable tool",
parameters: { type: "object", properties: {} },
async execute() {
return { content: [{ type: "text", text: "ok" }] };
},
};
Object.defineProperty(tool, "parameters", {
enumerable: true,
get() {
throw new Error("fuzzplugin parameters getter exploded");
},
});
expect(
capturePluginToolDescriptor({
pluginId: "fuzzplugin",
tool: tool as never,
optional: false,
}),
).toBeUndefined();
});
});
describe("plugin tool descriptor cache keys", () => {
afterEach(() => {
hoisted.resolveRuntimeConfigCacheKey.mockClear();

View File

@@ -145,21 +145,27 @@ export function capturePluginToolDescriptor(params: {
pluginId: string;
tool: AnyAgentTool;
optional: boolean;
}): CachedPluginToolDescriptor {
const label = (params.tool as { label?: unknown }).label;
const title = typeof label === "string" && label.trim() ? label.trim() : undefined;
return {
...(params.tool.displaySummary ? { displaySummary: params.tool.displaySummary } : {}),
optional: params.optional,
descriptor: {
name: params.tool.name,
...(title ? { title } : {}),
description: params.tool.description,
inputSchema: asJsonObject(params.tool.parameters),
owner: { kind: "plugin", pluginId: params.pluginId },
executor: { kind: "plugin", pluginId: params.pluginId, toolName: params.tool.name },
},
};
}): CachedPluginToolDescriptor | undefined {
try {
const label = (params.tool as { label?: unknown }).label;
const title = typeof label === "string" && label.trim() ? label.trim() : undefined;
const displaySummary = params.tool.displaySummary;
const name = params.tool.name;
return {
...(displaySummary ? { displaySummary } : {}),
optional: params.optional,
descriptor: {
name,
...(title ? { title } : {}),
description: params.tool.description,
inputSchema: asJsonObject(params.tool.parameters),
owner: { kind: "plugin", pluginId: params.pluginId },
executor: { kind: "plugin", pluginId: params.pluginId, toolName: name },
},
};
} catch {
return undefined;
}
}
export function readCachedPluginToolDescriptors(

View File

@@ -163,6 +163,22 @@ function createMalformedTool(name: string) {
};
}
function createToolWithUnstableDescriptorSchema(name: string) {
let parameterReads = 0;
const tool = makeTool(name);
Object.defineProperty(tool, "parameters", {
enumerable: true,
get() {
parameterReads += 1;
if (parameterReads > 1) {
throw new Error("fuzzplugin parameters getter exploded");
}
return { type: "object", properties: {} };
},
});
return tool;
}
function installConsoleMethodSpy(method: "log" | "warn") {
const spy = vi.fn();
loggingState.rawConsole = {
@@ -2061,6 +2077,28 @@ describe("resolvePluginTools optional tools", () => {
expect(factory).toHaveBeenCalledTimes(2);
});
it("keeps live tools when descriptor cache capture hits an unstable schema getter", () => {
const factory = vi.fn(() =>
createToolWithUnstableDescriptorSchema("unstable_descriptor_tool"),
);
setRegistry([
{
pluginId: "cache-fuzz",
optional: false,
source: "/tmp/cache-fuzz.js",
names: ["unstable_descriptor_tool"],
factory,
},
]);
const first = resolvePluginTools(createResolveToolsParams());
const second = resolvePluginTools(createResolveToolsParams());
expectResolvedToolNames(first, ["unstable_descriptor_tool"]);
expectResolvedToolNames(second, ["unstable_descriptor_tool"]);
expect(factory).toHaveBeenCalledTimes(2);
});
it("executes cached healthy tools when a runtime sibling is malformed", async () => {
const factory = vi.fn(() => [
createMalformedTool("fuzz_move_angles"),

View File

@@ -1333,14 +1333,15 @@ export function resolvePluginTools(params: {
});
if (manifestPlugin) {
const capturedDescriptors = capturedDescriptorsByPluginId.get(entry.pluginId) ?? [];
capturedDescriptors.push(
capturePluginToolDescriptor({
pluginId: entry.pluginId,
tool,
optional,
}),
);
capturedDescriptorsByPluginId.set(entry.pluginId, capturedDescriptors);
const capturedDescriptor = capturePluginToolDescriptor({
pluginId: entry.pluginId,
tool,
optional,
});
if (capturedDescriptor) {
capturedDescriptors.push(capturedDescriptor);
capturedDescriptorsByPluginId.set(entry.pluginId, capturedDescriptors);
}
}
tools.push(tool);
}