fix(core): validate tool definitions on registration (#42117)

Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
This commit is contained in:
opencode-agent[bot] 2026-08-12 14:08:13 -05:00 committed by GitHub
parent c6a86acb0b
commit 8d5dd206a6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 37 additions and 2 deletions

View file

@ -2,7 +2,7 @@ export * as Tool from "./tool.js"
export { CallID, Content, Error, FileContent, TextContent } from "@opencode-ai/schema/tool"
export type { Context, Metadata, Options, Result } from "@opencode-ai/schema/tool"
import type { ToolCall, ToolDefinition } from "@opencode-ai/ai"
import { ToolDefinition, type ToolCall } from "@opencode-ai/ai"
import { Tool } from "@opencode-ai/schema/tool"
import { Context, Effect, Layer, Schema, Scope, Semaphore } from "effect"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
@ -100,7 +100,7 @@ const layer = Layer.effect(
const execution = yield* execute(tool, beforeEvent.input, context).pipe(
Effect.map((value) => ({ value })),
Effect.catchTag("Tool.Error", (failure) => Effect.succeed({ failure })),
)
)
const base = {
tool: name,
sessionID: context.sessionID,
@ -165,6 +165,19 @@ const layer = Layer.effect(
}),
)
if (entries.length === 0) return
yield* Effect.forEach(
entries,
(entry) =>
Effect.try({
try: () => ToolDefinition.make(definition(entry.tool)),
catch: (error) =>
new RegistrationError({
name: entry.key,
message: `Invalid tool definition ${entry.key}: ${error instanceof Error ? error.message : String(error)}`,
}),
}),
{ discard: true },
)
yield* Effect.uninterruptible(
lock.withPermit(
Effect.gen(function* () {

View file

@ -110,6 +110,28 @@ describe("Tool", () => {
}),
)
it.effect("rejects invalid tool definitions before installing any tools", () =>
Effect.gen(function* () {
const service = yield* Tool.Service
const error = yield* service
.transform((draft) => {
draft.add({ ...make(), name: "healthy", options: { codemode: false } })
draft.add({
name: "phone_type",
input: Schema.Struct({}),
execute: () => Effect.succeed({ content: "ok" }),
options: { codemode: false },
} as unknown as Info)
})
.pipe(Effect.flip)
expect(error).toBeInstanceOf(Tool.RegistrationError)
expect(error.name).toBe("phone_type")
expect(error.message).toContain('Expected string, got undefined\n at ["description"]')
expect((yield* service.snapshot()).definitions.map((tool) => tool.name)).toEqual(["execute"])
}),
)
it.effect("canonicalizes effective definitions and keeps Code Mode last", () =>
Effect.gen(function* () {
const service = yield* Tool.Service