fix(codex): quarantine unreadable dynamic tool schemas

This commit is contained in:
Vincent Koc
2026-06-05 01:34:15 +02:00
parent 03f859237a
commit 6d7a1b8d0e
6 changed files with 174 additions and 14 deletions

View File

@@ -1,2 +1,2 @@
a3ab01b572937539e563aa320ad80135bc701e20fffc43c0351d799590b7a0e0 plugin-sdk-api-baseline.json
9d49587923f8fc4abb16d981bcab54acbf90a3e74ab05933761049e2da0cffe1 plugin-sdk-api-baseline.jsonl
df5631cfd3dbec340d258f06e2e62be6f03cf3d886efbf0463d915ffbafdacc2 plugin-sdk-api-baseline.json
fbcbb72b5e63d5824e8851265b4b5896ff5d21a761604511269ad27dbe107fd5 plugin-sdk-api-baseline.jsonl

View File

@@ -372,6 +372,35 @@ describe("createCodexDynamicToolBridge", () => {
expect(badExecute).not.toHaveBeenCalled();
});
it("quarantines unreadable dynamic tool schemas before bridge construction", () => {
const badExecute = vi.fn();
const badTool = createTool({
name: "dofbot_move_angles",
execute: badExecute,
});
Object.defineProperty(badTool, "parameters", {
enumerable: true,
get() {
throw new Error("dofbot schema getter exploded");
},
});
const bridge = createCodexDynamicToolBridge({
tools: [badTool, createTool({ name: "message" })],
signal: new AbortController().signal,
});
expect(bridge.availableSpecs.map((tool) => tool.name)).toEqual(["message"]);
expect(bridge.specs.map((tool) => tool.name)).toEqual(["message"]);
expect(bridge.telemetry.quarantinedTools).toEqual([
{
tool: "dofbot_move_angles",
violations: ["dofbot_move_angles.inputSchema is unreadable"],
},
]);
expect(badExecute).not.toHaveBeenCalled();
});
it("can expose all dynamic tools directly for compatibility", () => {
const bridge = createCodexDynamicToolBridge({
tools: [createTool({ name: "web_search" }), createTool({ name: "message" })],

View File

@@ -15,7 +15,7 @@ import {
isMessagingTool,
isMessagingToolSendAction,
normalizeHeartbeatToolResponse,
projectRuntimeToolInputSchema,
projectRuntimeCompatibleToolInputSchemas,
runAgentHarnessAfterToolCallHook,
setBeforeToolCallDiagnosticsEnabled,
type AnyAgentTool,
@@ -322,17 +322,19 @@ function projectCodexDynamicTools(tools: readonly AnyAgentTool[]): {
tools: ProjectedCodexDynamicTool[];
quarantinedTools: CodexDynamicToolSchemaQuarantine[];
} {
const projectedTools: ProjectedCodexDynamicTool[] = [];
const quarantinedTools: CodexDynamicToolSchemaQuarantine[] = [];
for (const tool of tools) {
const projection = projectRuntimeToolInputSchema(tool.parameters, `${tool.name}.inputSchema`);
if (projection.violations.length > 0) {
quarantinedTools.push({ tool: tool.name, violations: projection.violations });
continue;
}
projectedTools.push({ tool, inputSchema: projection.schema as JsonValue });
}
return { tools: projectedTools, quarantinedTools };
const projection = projectRuntimeCompatibleToolInputSchemas(tools, {
schemaLabel: "inputSchema",
});
return {
tools: projection.tools.map(({ tool, schema }) => ({
tool,
inputSchema: schema as JsonValue,
})),
quarantinedTools: projection.diagnostics.map((diagnostic) => ({
tool: diagnostic.toolName,
violations: diagnostic.violations,
})),
};
}
function warnQuarantinedDynamicTools(tools: readonly CodexDynamicToolSchemaQuarantine[]): void {

View File

@@ -5,6 +5,7 @@ import {
filterProviderNormalizableTools,
filterRuntimeCompatibleTools,
inspectRuntimeToolInputSchemas,
projectRuntimeCompatibleToolInputSchemas,
projectRuntimeToolInputSchema,
} from "./tool-schema-projection.js";
import type { AnyAgentTool } from "./tools/common.js";
@@ -166,6 +167,43 @@ describe("runtime tool input schema projection", () => {
});
});
it("projects compatible schemas while quarantining unreadable runtime fields", () => {
const unreadable = {
name: "fuzzplugin_unreadable",
parameters: { type: "object", properties: {} },
};
Object.defineProperty(unreadable, "parameters", {
enumerable: true,
get() {
throw new Error("fuzzplugin parameters getter exploded");
},
});
const healthy = {
name: "healthy",
parameters: { type: "object", properties: { query: { type: "string" } } },
};
expect(
projectRuntimeCompatibleToolInputSchemas([unreadable, healthy], {
schemaLabel: "inputSchema",
}),
).toEqual({
tools: [
{
tool: healthy,
schema: { type: "object", properties: { query: { type: "string" } } },
},
],
diagnostics: [
{
toolName: "fuzzplugin_unreadable",
toolIndex: 0,
violations: ["fuzzplugin_unreadable.inputSchema is unreadable"],
},
],
});
});
it("keeps provider-normalizable object schemas for provider-specific cleanup", () => {
const dynamicSchema = {
name: "fuzzplugin_dynamic_ref",

View File

@@ -20,6 +20,14 @@ export type RuntimeToolInputSchemaProjection = {
readonly violations: readonly string[];
};
/** Runtime tool paired with its projected JSON-safe schema. */
export type RuntimeToolInputSchemaProjectedTool<
TTool extends Pick<AnyAgentTool, "name" | "parameters">,
> = {
readonly tool: TTool;
readonly schema: RuntimeToolInputSchemaJson;
};
/** Diagnostic for one incompatible runtime tool schema. */
export type RuntimeToolSchemaDiagnostic = {
readonly toolName: string;
@@ -33,6 +41,14 @@ export type RuntimeToolSchemaInspection<TTool extends Pick<AnyAgentTool, "name"
readonly diagnostics: readonly RuntimeToolSchemaDiagnostic[];
};
/** Runtime tool list split into projected compatible tools and schema diagnostics. */
export type RuntimeToolInputSchemaProjectionInspection<
TTool extends Pick<AnyAgentTool, "name" | "parameters">,
> = {
readonly tools: readonly RuntimeToolInputSchemaProjectedTool<TTool>[];
readonly diagnostics: readonly RuntimeToolSchemaDiagnostic[];
};
type RuntimeToolEntryRead<TTool extends Pick<AnyAgentTool, "name" | "parameters">> =
| {
readonly ok: true;
@@ -46,6 +62,10 @@ type RuntimeToolEntryRead<TTool extends Pick<AnyAgentTool, "name" | "parameters"
type ToolSchemaInspectionMode = "runtime" | "provider-normalizable";
type RuntimeToolProjectionOptions = {
schemaLabel?: string;
};
function unreadableRuntimeToolEntry(
toolIndex: number,
): RuntimeToolEntryRead<Pick<AnyAgentTool, "name" | "parameters">> {
@@ -250,6 +270,50 @@ function inspectToolSchema(
return violations.length > 0 ? { toolName, toolIndex, violations } : undefined;
}
function projectToolSchema<TTool extends Pick<AnyAgentTool, "name" | "parameters">>(
tool: TTool,
toolIndex: number,
options: RuntimeToolProjectionOptions,
):
| { ok: true; tool: TTool; schema: RuntimeToolInputSchemaJson }
| { ok: false; diagnostic: RuntimeToolSchemaDiagnostic } {
const schemaLabel = options.schemaLabel ?? "parameters";
const nameRead = readToolProjectionField(tool, "name");
const toolName =
nameRead.readable && typeof nameRead.value === "string" && nameRead.value
? nameRead.value
: `tool[${toolIndex}]`;
const descriptorViolations = nameRead.readable ? [] : [`${toolName}.name is unreadable`];
const parametersRead = readToolProjectionField(tool, "parameters");
if (!parametersRead.readable) {
return {
ok: false,
diagnostic: {
toolName,
toolIndex,
violations: [...descriptorViolations, `${toolName}.${schemaLabel} is unreadable`],
},
};
}
const projection = projectRuntimeToolInputSchema(
parametersRead.value,
`${toolName}.${schemaLabel}`,
);
const violations = [...descriptorViolations, ...projection.violations];
if (violations.length > 0) {
return {
ok: false,
diagnostic: {
toolName,
toolIndex,
violations,
},
};
}
return { ok: true, tool, schema: projection.schema };
}
function inspectToolEntries<TTool extends Pick<AnyAgentTool, "name" | "parameters">>(
entries: readonly RuntimeToolEntryRead<TTool>[],
mode: ToolSchemaInspectionMode,
@@ -285,6 +349,30 @@ export function filterRuntimeCompatibleTools<
return inspectToolEntries(readRuntimeToolEntries(tools), "runtime");
}
/** Projects and filters tools to schemas accepted by runtime dynamic tool surfaces. */
export function projectRuntimeCompatibleToolInputSchemas<
TTool extends Pick<AnyAgentTool, "name" | "parameters">,
>(
tools: readonly TTool[],
options: RuntimeToolProjectionOptions = {},
): RuntimeToolInputSchemaProjectionInspection<TTool> {
const diagnostics: RuntimeToolSchemaDiagnostic[] = [];
const projectedTools: RuntimeToolInputSchemaProjectedTool<TTool>[] = [];
for (const entry of readRuntimeToolEntries(tools)) {
if (!entry.ok) {
diagnostics.push(entry.diagnostic);
continue;
}
const projection = projectToolSchema(entry.tool, entry.toolIndex, options);
if (projection.ok) {
projectedTools.push({ tool: projection.tool, schema: projection.schema });
continue;
}
diagnostics.push(projection.diagnostic);
}
return { tools: projectedTools, diagnostics };
}
/** Filters tools to those that providers can normalize before dispatch. */
export function filterProviderNormalizableTools<
TTool extends Pick<AnyAgentTool, "name" | "parameters">,

View File

@@ -178,8 +178,11 @@ export {
export {
filterProviderNormalizableTools,
inspectRuntimeToolInputSchemas,
projectRuntimeCompatibleToolInputSchemas,
projectRuntimeToolInputSchema,
type RuntimeToolInputSchemaJson,
type RuntimeToolInputSchemaProjectedTool,
type RuntimeToolInputSchemaProjectionInspection,
type RuntimeToolInputSchemaProjection,
type RuntimeToolSchemaDiagnostic,
} from "../agents/tool-schema-projection.js";