fix(core): allow non-letter tool name prefixes (#45317)

This commit is contained in:
Aiden Cline
2026-08-26 11:21:14 -05:00
committed by GitHub
parent fedf017e25
commit cf98ca55c9
2 changed files with 67 additions and 4 deletions
+2 -2
View File
@@ -271,12 +271,12 @@ function schemaMakeError(error: unknown) {
}
const validateName = (name: string) =>
/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(name)
/^[A-Za-z0-9_-]{1,64}$/.test(name)
? Effect.void
: Effect.fail(new RegistrationError({ name, message: `Invalid tool name: ${name}` }))
const validateNamespace = (namespace: string) =>
namespace.split(".").every((segment) => /^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(segment))
namespace.split(".").every((segment) => /^[A-Za-z0-9_-]{1,64}$/.test(segment))
? Effect.void
: Effect.fail(
new RegistrationError({
@@ -85,8 +85,10 @@ describe("Tool", () => {
it.effect("rejects invalid and colliding normalized names", () =>
Effect.gen(function* () {
const service = yield* Tool.Service
const invalid = yield* transform(service, { "123": make() }, { codemode: false }).pipe(Effect.flip)
expect(invalid.message).toBe("Invalid tool name: 123")
for (const name of ["", "x".repeat(65)]) {
const invalid = yield* transform(service, { [name]: make() }, { codemode: false }).pipe(Effect.flip)
expect(invalid.message).toBe(`Invalid tool name: ${name}`)
}
const collision = yield* transform(service, { "echo.tool": make(), echo_tool: make() }, { codemode: false }).pipe(
Effect.flip,
@@ -96,6 +98,67 @@ describe("Tool", () => {
}),
)
it.effect("executes native tools without requiring letter-leading names or namespace segments", () =>
Effect.gen(function* () {
const service = yield* Tool.Service
yield* transform(
service,
{ "2d_get_scene": make(), "123": make(), _lookup: make(), "-lookup": make() },
{ codemode: false },
)
yield* transform(service, { "2d_get_scene": make() }, { namespace: "123._private.-tools", codemode: false })
const snapshot = yield* service.snapshot()
expect(snapshot.definitions.map((tool) => tool.name)).toEqual([
"-lookup",
"123",
"123__private_-tools_2d_get_scene",
"2d_get_scene",
"_lookup",
"execute",
])
for (const name of ["2d_get_scene", "123", "_lookup", "-lookup", "123__private_-tools_2d_get_scene"]) {
expect((yield* snapshot.execute(call(name))).output).toEqual({ text: name })
}
}),
)
it.effect("executes Code Mode tools without requiring letter-leading names or namespace segments", () =>
Effect.gen(function* () {
const service = yield* Tool.Service
yield* transform(service, { "2d_get_scene": make(), "123": make(), _lookup: make(), "-lookup": make() })
yield* transform(service, { "2d_get_scene": make() }, { namespace: "123._private.-tools", codemode: true })
const snapshot = yield* service.snapshot()
expect(snapshot.definitions.map((tool) => tool.name)).toEqual(["execute"])
expect(snapshot.codeModeCatalog?.map((tool) => tool.path)).toEqual([
"-lookup",
"123",
"123._private.-tools.2d_get_scene",
"2d_get_scene",
"_lookup",
])
const result = yield* snapshot.execute({
...call("execute"),
call: {
type: "tool-call",
id: "call-nonletter-names",
name: "execute",
input: {
code: `const results = await Promise.all([
tools["2d_get_scene"]({ text: "digit" }),
tools["123"]({ text: "numeric" }),
tools._lookup({ text: "underscore" }),
tools["-lookup"]({ text: "hyphen" }),
tools["123"]._private["-tools"]["2d_get_scene"]({ text: "namespaced" }),
]); return results.map(result => result.text).join(",");`,
},
},
})
expect(result.content).toEqual([{ type: "text", text: "digit,numeric,underscore,hyphen,namespaced" }])
}),
)
it.effect("validates a registration batch before installing any tools", () =>
Effect.gen(function* () {
const service = yield* Tool.Service