diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index e14adb45b..06e400d1e 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -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 diff --git a/packages/coding-agent/src/core/extensions/loader.ts b/packages/coding-agent/src/core/extensions/loader.ts index 32ca6c952..d06f801a4 100644 --- a/packages/coding-agent/src/core/extensions/loader.ts +++ b/packages/coding-agent/src/core/extensions/loader.ts @@ -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, diff --git a/packages/coding-agent/test/extensions-runner.test.ts b/packages/coding-agent/test/extensions-runner.test.ts index 70f06a3cf..d33b2668f 100644 --- a/packages/coding-agent/test/extensions-runner.test.ts +++ b/packages/coding-agent/test/extensions-runner.test.ts @@ -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";