Skip to content

Commit 748510b

Browse files
committed
fix(doctor): validate tool schemas for configured agents
1 parent 45e6af5 commit 748510b

3 files changed

Lines changed: 287 additions & 104 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai
2222
### Fixes
2323

2424
- Security/CLI/runtime: harden hostname normalization for repeated trailing dots, block side-effecting command wrappers, reject unsafe Node runtime env overrides, reject loose numeric CLI and gateway options, require admin approval for node device-role pairing, and reject no-auth Tailscale exposure. (#87305, #87292, #87308, #87146) Thanks @pgondhi987.
25+
- Doctor: validate runtime tool schemas for every configured embedded agent while skipping ACP-only profiles, so bad non-default plugin or MCP tools are reported before assistant turns.
2526
- Telegram: route `sendMessage` action replies through durable outbound delivery so completed agent responses remain retryable when the gateway send path times out. (#87261) Thanks @mbelinky.
2627
- Matrix/auto-reply: keep draft previews mention-inert, preserve final mention delivery, send mention finals normally, await shared DM notices, ignore filename-embedded MXIDs, and suppress reasoning-prefixed `NO_REPLY` responses.
2728
- Agents/providers: add OpenAI-compatible cache retention, forward cached token usage in chat completions, preserve runtime context before active user turns, strip stale Anthropic thinking, load Claude CLI OAuth for Pi auth profiles, avoid false Codex runtime live switches, and quarantine unsupported tool schemas. (#82062, #87167, #86855)

src/flows/doctor-core-checks.runtime.test.ts

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ describe("doctor runtime tool schema checks", () => {
9191
checkId: "core/doctor/runtime-tool-schemas",
9292
severity: "error",
9393
message:
94-
"Tool dofbot__dofbot_move_angles from plugin bundle-mcp has an unsupported input schema for runtime projection.",
94+
"Agent main tool dofbot__dofbot_move_angles from plugin bundle-mcp has an unsupported input schema for runtime projection.",
9595
path: "mcp.servers",
9696
target: "dofbot__dofbot_move_angles",
9797
requirement: 'dofbot__dofbot_move_angles.parameters.type must be "object"',
@@ -101,6 +101,128 @@ describe("doctor runtime tool schema checks", () => {
101101
expect(mocks.disposeBundleRuntime).toHaveBeenCalledTimes(1);
102102
});
103103

104+
it("reports unsupported schemas exposed only to a non-default configured agent", async () => {
105+
mocks.createOpenClawCodingTools.mockImplementation((options) =>
106+
options?.agentId === "worker"
107+
? [tool("dofbot_move_angles", { type: "array", items: { type: "number" } })]
108+
: [tool("healthy", { type: "object", properties: {} })],
109+
);
110+
111+
await expect(
112+
collectRuntimeToolSchemaFindings({
113+
agents: {
114+
list: [
115+
{ id: "main", default: true, workspace: "/tmp/shared-workspace" },
116+
{ id: "worker", workspace: "/tmp/shared-workspace" },
117+
],
118+
},
119+
}),
120+
).resolves.toContainEqual({
121+
checkId: "core/doctor/runtime-tool-schemas",
122+
severity: "error",
123+
message:
124+
"Agent worker tool dofbot_move_angles has an unsupported input schema for runtime projection.",
125+
path: "tools.dofbot_move_angles",
126+
target: "dofbot_move_angles",
127+
requirement: 'dofbot_move_angles.parameters.type must be "object"',
128+
fixHint:
129+
"Disable or update the offending plugin/tool so its parameters are a JSON object schema, then rerun doctor.",
130+
});
131+
expect(mocks.createOpenClawCodingTools).toHaveBeenCalledWith(
132+
expect.objectContaining({ agentId: "main" }),
133+
);
134+
expect(mocks.createOpenClawCodingTools).toHaveBeenCalledWith(
135+
expect.objectContaining({ agentId: "worker" }),
136+
);
137+
expect(mocks.createBundleMcpToolRuntime).toHaveBeenCalledTimes(1);
138+
expect(mocks.disposeBundleRuntime).toHaveBeenCalledTimes(1);
139+
});
140+
141+
it("skips ACP-only agents because they do not use embedded tool projection", async () => {
142+
mocks.createOpenClawCodingTools.mockImplementation((options) =>
143+
options?.agentId === "acp-worker"
144+
? [tool("dofbot_move_angles", { type: "array", items: { type: "number" } })]
145+
: [tool("healthy", { type: "object", properties: {} })],
146+
);
147+
mocks.createBundleMcpToolRuntime.mockImplementation(
148+
async (options: { workspaceDir: string }) => ({
149+
tools: options.workspaceDir.includes("acp")
150+
? [bundleMcpTool("dofbot__bad", { type: "array", items: { type: "number" } })]
151+
: [],
152+
dispose: mocks.disposeBundleRuntime,
153+
}),
154+
);
155+
156+
await expect(
157+
collectRuntimeToolSchemaFindings({
158+
agents: {
159+
list: [
160+
{ id: "main", default: true, workspace: "/tmp/main-workspace" },
161+
{
162+
id: "acp-worker",
163+
workspace: "/tmp/acp-workspace",
164+
runtime: { type: "acp" },
165+
},
166+
],
167+
},
168+
}),
169+
).resolves.toEqual([]);
170+
expect(mocks.createOpenClawCodingTools).toHaveBeenCalledTimes(1);
171+
expect(mocks.createOpenClawCodingTools).toHaveBeenCalledWith(
172+
expect.objectContaining({ agentId: "main" }),
173+
);
174+
expect(mocks.createBundleMcpToolRuntime).toHaveBeenCalledTimes(1);
175+
expect(mocks.createBundleMcpToolRuntime).toHaveBeenCalledWith(
176+
expect.objectContaining({ workspaceDir: expect.stringContaining("main-workspace") }),
177+
);
178+
});
179+
180+
it("loads bundled MCP runtime once per distinct agent workspace", async () => {
181+
mocks.createOpenClawCodingTools.mockReturnValue([]);
182+
mocks.createBundleMcpToolRuntime.mockImplementation(
183+
async (options: { workspaceDir: string }) => ({
184+
tools: options.workspaceDir.includes("worker")
185+
? [
186+
bundleMcpTool("dofbot__dofbot_move_angles", {
187+
type: "array",
188+
items: { type: "number" },
189+
}),
190+
]
191+
: [bundleMcpTool("healthy", { type: "object", properties: {} })],
192+
dispose: mocks.disposeBundleRuntime,
193+
}),
194+
);
195+
196+
await expect(
197+
collectRuntimeToolSchemaFindings({
198+
agents: {
199+
list: [
200+
{ id: "main", default: true, workspace: "/tmp/main-workspace" },
201+
{ id: "worker", workspace: "/tmp/worker-workspace" },
202+
],
203+
},
204+
}),
205+
).resolves.toContainEqual({
206+
checkId: "core/doctor/runtime-tool-schemas",
207+
severity: "error",
208+
message:
209+
"Agent worker tool dofbot__dofbot_move_angles from plugin bundle-mcp has an unsupported input schema for runtime projection.",
210+
path: "mcp.servers",
211+
target: "dofbot__dofbot_move_angles",
212+
requirement: 'dofbot__dofbot_move_angles.parameters.type must be "object"',
213+
fixHint:
214+
"Disable or update the offending MCP server/tool so its parameters are a JSON object schema, then rerun doctor.",
215+
});
216+
expect(mocks.createBundleMcpToolRuntime).toHaveBeenCalledTimes(2);
217+
expect(mocks.createBundleMcpToolRuntime).toHaveBeenCalledWith(
218+
expect.objectContaining({ workspaceDir: expect.stringContaining("main-workspace") }),
219+
);
220+
expect(mocks.createBundleMcpToolRuntime).toHaveBeenCalledWith(
221+
expect.objectContaining({ workspaceDir: expect.stringContaining("worker-workspace") }),
222+
);
223+
expect(mocks.disposeBundleRuntime).toHaveBeenCalledTimes(2);
224+
});
225+
104226
it("does not report bundle MCP schemas filtered out by the final runtime tool policy", async () => {
105227
mocks.createBundleMcpToolRuntime.mockResolvedValueOnce({
106228
tools: [

0 commit comments

Comments
 (0)