From 7a012cac08c0e198bbffb48dcfd75a4f908d75e7 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 14 May 2026 23:36:28 -0400 Subject: [PATCH] fix(tool): ignore invalid custom tool exports --- packages/opencode/src/tool/registry.ts | 7 ++++- packages/opencode/test/tool/registry.test.ts | 27 ++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 56df754afe..5869b50a2a 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -201,7 +201,8 @@ export const layer: Layer.Layer< // `match` is an absolute filesystem path from `Glob.scanSync(..., { absolute: true })`. // Import it as `file://` so Node on Windows accepts the dynamic import. const mod = yield* Effect.promise(() => import(pathToFileURL(match).href)) - for (const [id, def] of Object.entries(mod)) { + for (const [id, def] of Object.entries(mod)) { + if (!isPluginTool(def)) continue custom.push(fromPlugin(id === "default" ? namespace : `${namespace}_${id}`, def)) } } @@ -396,6 +397,10 @@ function isZodType(value: unknown): value is z.ZodType { return typeof value === "object" && value !== null && "_zod" in value } +function isPluginTool(value: unknown): value is ToolDefinition { + return typeof value === "object" && value !== null && "args" in value && "description" in value && "execute" in value +} + function isJsonSchemaDefinition(value: unknown): value is JSONSchema7Definition { return typeof value === "boolean" || (typeof value === "object" && value !== null && !Array.isArray(value)) } diff --git a/packages/opencode/test/tool/registry.test.ts b/packages/opencode/test/tool/registry.test.ts index 3239855ca5..0a96a689cd 100644 --- a/packages/opencode/test/tool/registry.test.ts +++ b/packages/opencode/test/tool/registry.test.ts @@ -158,6 +158,33 @@ describe("tool.registry", () => { }), ) + it.instance("ignores non-tool exports in .opencode/tool files", () => + Effect.gen(function* () { + const test = yield* TestInstance + const tool = path.join(test.directory, ".opencode", "tool") + yield* Effect.promise(() => fs.mkdir(tool, { recursive: true })) + yield* Effect.promise(() => + Bun.write( + path.join(tool, "mixed.ts"), + [ + "export const helper = 'not a tool'", + "export default {", + " description: 'mixed tool',", + " args: {},", + " execute: async () => 'ok',", + "}", + "", + ].join("\n"), + ), + ) + + const registry = yield* ToolRegistry.Service + const ids = yield* registry.ids() + expect(ids).toContain("mixed") + expect(ids).not.toContain("mixed_helper") + }), + ) + it.instance("loads tools from .opencode/tools (plural)", () => Effect.gen(function* () { const test = yield* TestInstance