fix(coding-agent): validate extension tool parameter schemas

Reject malformed extension tools during registration instead of allowing them to break provider request serialization.

Fixes #9300
This commit is contained in:
David Brailovsky
2026-09-09 12:15:18 +02:00
parent c37b0e03b5
commit acaa253cc8
3 changed files with 32 additions and 0 deletions
+1
View File
@@ -17,6 +17,7 @@
- Fixed direct RPC `steer` and `follow_up` commands bypassing extension `input` handlers ([#8718](https://github.com/earendil-works/pi/issues/8718)).
- Fixed premature missing-model errors after login by waiting for catalog discovery. Radius now defaults to `balanced`, falling back to the first available Radius model when needed.
- Fixed fullscreen mode reserving a blank row for custom footers that render zero rows ([#8919](https://github.com/earendil-works/pi/issues/8919)).
- Fixed extension tools without parameter schemas to be rejected during registration instead of breaking provider requests ([#9300](https://github.com/earendil-works/pi/issues/9300)).
## [0.85.1] - 2026-09-05
@@ -286,6 +286,11 @@ function createExtensionAPI(
registerTool(tool: ToolDefinition): void {
assertActive();
if (typeof tool.parameters !== "object" || tool.parameters === null || Array.isArray(tool.parameters)) {
throw new Error(
`Tool "${tool.name}" registered by extension "${extension.path}" must define an object parameter schema.`,
);
}
extension.tools.set(tool.name, {
definition: tool,
sourceInfo: extension.sourceInfo,
@@ -390,6 +390,32 @@ describe("ExtensionRunner", () => {
expect(tools.map((t) => t.definition.name).sort()).toEqual(["tool_a", "tool_b"]);
});
// Regression test for #9300.
it("rejects extension tools without a parameter schema", async () => {
const extensionPath = path.join(extensionsDir, "missing-parameters.js");
fs.writeFileSync(
extensionPath,
`export default function(pi) {
pi.registerTool({
name: "noop",
label: "No-op",
description: "Do nothing",
execute: async () => ({ content: [{ type: "text", text: "ok" }] }),
});
}`,
);
const result = await loadExtensions([extensionPath], tempDir);
expect(result.extensions).toHaveLength(0);
expect(result.errors).toEqual([
{
path: extensionPath,
error: `Failed to load extension: Tool "noop" registered by extension "${extensionPath}" must define an object parameter schema.`,
},
]);
});
it("keeps first tool when two extensions register the same name", async () => {
const first = `
import { Type } from "typebox";