Skip to content

Commit 26135a1

Browse files
committed
fix(codex): quarantine unreadable dynamic tools
1 parent 7b82901 commit 26135a1

2 files changed

Lines changed: 280 additions & 37 deletions

File tree

extensions/codex/src/app-server/dynamic-tools.test.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,116 @@ describe("createCodexDynamicToolBridge", () => {
371371
expect(badExecute).not.toHaveBeenCalled();
372372
});
373373

374+
it("quarantines unreadable dynamic tool descriptors without dropping healthy siblings", () => {
375+
const warn = vi.spyOn(embeddedAgentLog, "warn").mockImplementation(() => undefined);
376+
const poisonedName = createTool({
377+
name: "fuzzplugin_unreadable_name",
378+
execute: vi.fn(),
379+
});
380+
Object.defineProperty(poisonedName, "name", {
381+
enumerable: true,
382+
get() {
383+
throw new Error("fuzzplugin dynamic tool name getter exploded");
384+
},
385+
});
386+
const poisonedSchema = createTool({
387+
name: "fuzzplugin_unreadable_schema",
388+
execute: vi.fn(),
389+
});
390+
Object.defineProperty(poisonedSchema, "parameters", {
391+
enumerable: true,
392+
get() {
393+
throw new Error("fuzzplugin dynamic tool schema getter exploded");
394+
},
395+
});
396+
const invalidName = createTool({
397+
name: "",
398+
execute: vi.fn(),
399+
});
400+
const poisonedExecute = createTool({
401+
name: "fuzzplugin_unreadable_execute",
402+
});
403+
Object.defineProperty(poisonedExecute, "execute", {
404+
enumerable: true,
405+
get() {
406+
throw new Error("fuzzplugin dynamic tool execute getter exploded");
407+
},
408+
});
409+
410+
const bridge = createCodexDynamicToolBridge({
411+
tools: [
412+
poisonedName,
413+
poisonedSchema,
414+
invalidName,
415+
poisonedExecute,
416+
createTool({ name: "message" }),
417+
],
418+
signal: new AbortController().signal,
419+
});
420+
421+
expect(bridge.availableSpecs.map((tool) => tool.name)).toEqual(["message"]);
422+
expect(bridge.specs.map((tool) => tool.name)).toEqual(["message"]);
423+
expect(bridge.telemetry.quarantinedTools).toEqual([
424+
{
425+
tool: "tool[0]",
426+
violations: ["tool[0].name is unreadable"],
427+
},
428+
{
429+
tool: "fuzzplugin_unreadable_schema",
430+
violations: ["fuzzplugin_unreadable_schema.inputSchema is unreadable"],
431+
},
432+
{
433+
tool: "tool[2]",
434+
violations: ["tool[2].name must be a non-empty string"],
435+
},
436+
{
437+
tool: "fuzzplugin_unreadable_execute",
438+
violations: [
439+
"fuzzplugin_unreadable_execute could not be wrapped for before-tool-call hooks",
440+
],
441+
},
442+
]);
443+
expect(warn).toHaveBeenCalledWith(
444+
expect.stringContaining(
445+
"tool[0], fuzzplugin_unreadable_schema, tool[2], fuzzplugin_unreadable_execute",
446+
),
447+
expect.objectContaining({
448+
tools: [
449+
{
450+
tool: "tool[0]",
451+
violations: ["tool[0].name is unreadable"],
452+
},
453+
{
454+
tool: "fuzzplugin_unreadable_schema",
455+
violations: ["fuzzplugin_unreadable_schema.inputSchema is unreadable"],
456+
},
457+
{
458+
tool: "tool[2]",
459+
violations: ["tool[2].name must be a non-empty string"],
460+
},
461+
{
462+
tool: "fuzzplugin_unreadable_execute",
463+
violations: [
464+
"fuzzplugin_unreadable_execute could not be wrapped for before-tool-call hooks",
465+
],
466+
},
467+
],
468+
}),
469+
);
470+
471+
const registeredBridge = createCodexDynamicToolBridge({
472+
tools: [poisonedExecute, createTool({ name: "message" })],
473+
registeredTools: [
474+
createTool({ name: "fuzzplugin_unreadable_execute" }),
475+
createTool({ name: "message" }),
476+
],
477+
signal: new AbortController().signal,
478+
});
479+
480+
expect(registeredBridge.availableSpecs.map((tool) => tool.name)).toEqual(["message"]);
481+
expect(registeredBridge.specs.map((tool) => tool.name)).toEqual(["message"]);
482+
});
483+
374484
it("can expose all dynamic tools directly for compatibility", () => {
375485
const bridge = createCodexDynamicToolBridge({
376486
tools: [createTool({ name: "web_search" }), createTool({ name: "message" })],

0 commit comments

Comments
 (0)