Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/core/src/tool/application-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const layer = Layer.effect(
const entries = Object.entries(tools)
if (entries.length === 0) return
yield* Effect.forEach(entries, ([name]) => Tool.validateName(name), { discard: true })
yield* Effect.forEach(entries, ([name, tool]) => Tool.validate(name, tool), { discard: true })
const registrations = entries.map(([name, tool]) => [name, { identity: {}, tool }] as const)
yield* state.transform((draft) => {
for (const [name, entry] of registrations) draft.set(name, entry)
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/tool/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { SessionSchema } from "../session/schema"
import { ToolOutputStore } from "../tool-output-store"
import { Wildcard } from "../util/wildcard"
import { ApplicationTools } from "./application-tools"
import { definition, permission, settle, validateName, type AnyTool, type RegistrationError } from "./tool"
import { definition, permission, settle, validate, validateName, type AnyTool, type RegistrationError } from "./tool"
import { Tools } from "./tools"
import { makeLocationNode } from "../effect/app-node"

Expand Down Expand Up @@ -86,6 +86,7 @@ const registryLayer = Layer.effect(
const entries = Object.entries(tools)
if (entries.length === 0) return
yield* Effect.forEach(entries, ([name]) => validateName(name), { discard: true })
yield* Effect.forEach(entries, ([name, tool]) => validate(name, tool), { discard: true })
yield* Effect.uninterruptible(
Effect.gen(function* () {
const token = {}
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/tool/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,11 @@ export const validateName = (name: string) =>
? Effect.void
: Effect.fail(new RegistrationError({ name, message: `Invalid tool name: ${name}` }))

export const validate = (name: string, tool: AnyTool) =>
runtimes.has(tool)
? Effect.void
: Effect.fail(new RegistrationError({ name, message: "Invalid Tool value" }))

export const withPermission = <Input extends SchemaType<any>, Output extends SchemaType<any>>(
tool: Definition<Input, Output>,
permission: string,
Expand Down
22 changes: 22 additions & 0 deletions packages/core/test/application-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,28 @@ describe("ApplicationTools", () => {
}),
)

it.effect("rejects invalid opaque tools before mutating application registrations", () =>
Effect.gen(function* () {
const applications = yield* ApplicationTools.Service
const registry = yield* ToolRegistry.Service
const healthy = contextual([])
yield* applications.register({ healthy })

const error = yield* applications
.register({
ok: contextual([]),
broken: structuredClone(healthy) as Tool.AnyTool,
})
.pipe(Effect.flip)
expect(error).toBeInstanceOf(Tool.RegistrationError)
expect(error.name).toBe("broken")
expect(error.message).toBe("Invalid Tool value")

expect([...applications.entries().keys()]).toEqual(["healthy"])
expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["healthy"])
}),
)

it.effect("filters an application tool by its name without adding execution authorization", () =>
Effect.gen(function* () {
const applications = yield* ApplicationTools.Service
Expand Down
26 changes: 26 additions & 0 deletions packages/core/test/session-runner-tool-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,32 @@ describe("ToolRegistry", () => {
}),
)

it.effect("rejects invalid opaque tools before mutating registrations", () =>
Effect.gen(function* () {
const service = yield* ToolRegistry.Service
yield* service.register({ healthy: make() })

const error = yield* service
.register({
ok: make(),
broken: structuredClone(make()) as Tool.AnyTool,
})
.pipe(Effect.flip)
expect(error).toBeInstanceOf(Tool.RegistrationError)
expect(error.name).toBe("broken")
expect(error.message).toBe("Invalid Tool value")

expect((yield* toolDefinitions(service)).map((tool) => tool.name)).toEqual(["healthy"])
expect(
yield* executeTool(service, {
sessionID,
...identity,
call: { type: "tool-call", id: "call-healthy", name: "healthy", input: { text: "ok" } },
}),
).toEqual({ type: "text", value: "ok" })
}),
)

it.effect("reuses model definitions across provider turns", () =>
Effect.gen(function* () {
const service = yield* ToolRegistry.Service
Expand Down
Loading