mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix(codex): quarantine unreadable dynamic tool schemas
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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" })],
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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">,
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user