From cf98ca55c9efe23868953187ccd3a0ce90ba1bb5 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Wed, 26 Aug 2026 11:21:14 -0500 Subject: [PATCH] fix(core): allow non-letter tool name prefixes (#45317) --- packages/core/src/tool.ts | 4 +- .../test/session-runner-tool-registry.test.ts | 67 ++++++++++++++++++- 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/packages/core/src/tool.ts b/packages/core/src/tool.ts index 13e85ee9d15..a31352d4e51 100644 --- a/packages/core/src/tool.ts +++ b/packages/core/src/tool.ts @@ -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({ diff --git a/packages/core/test/session-runner-tool-registry.test.ts b/packages/core/test/session-runner-tool-registry.test.ts index 7c6324345c6..69b8d936006 100644 --- a/packages/core/test/session-runner-tool-registry.test.ts +++ b/packages/core/test/session-runner-tool-registry.test.ts @@ -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