-
Notifications
You must be signed in to change notification settings - Fork 1.1k
[Bug] Accept unambiguous bare tool-name echoes for namespaced tools (Command Code muse family) #4729
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Bug] Accept unambiguous bare tool-name echoes for namespaced tools (Command Code muse family) #4729
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,125 @@ | ||||||||||||
| import { describe, expect, test } from "bun:test"; | ||||||||||||
| import { parseRequest } from "../../src/responses/parser"; | ||||||||||||
| import { buildToolBridgeMaps } from "../../src/server/responses"; | ||||||||||||
|
|
||||||||||||
| function collabRequest(bareName: string) { | ||||||||||||
| return parseRequest({ | ||||||||||||
| model: "meta/muse-spark-1.3-contributor", | ||||||||||||
| input: [ | ||||||||||||
| { type: "additional_tools", role: "developer", tools: [ | ||||||||||||
| { type: "namespace", name: "collaboration", tools: [ | ||||||||||||
| { type: "function", name: bareName, description: bareName, strict: false, parameters: { type: "object", properties: {}, required: [] } }, | ||||||||||||
| ] }, | ||||||||||||
| ] }, | ||||||||||||
| { type: "message", role: "user", content: [{ type: "input_text", text: "run it" }] }, | ||||||||||||
| ], | ||||||||||||
| } as any); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| describe("bare echo alias for namespaced tools (#4679)", () => { | ||||||||||||
| test("an unambiguous bare name is declared and restores to the namespaced identity", () => { | ||||||||||||
| const maps = buildToolBridgeMaps(collabRequest("list_agents") as any); | ||||||||||||
| expect(maps.declaredToolNames.has("list_agents")).toBe(true); | ||||||||||||
| expect(maps.toolNsMap.get("list_agents")).toEqual({ namespace: "collaboration", name: "list_agents" }); | ||||||||||||
| }); | ||||||||||||
|
|
||||||||||||
| test("a bare name claimed by two namespaces stays undeclared (no hijack)", () => { | ||||||||||||
| const parsed = parseRequest({ | ||||||||||||
| model: "meta/muse-spark-1.3-contributor", | ||||||||||||
| input: [ | ||||||||||||
| { type: "additional_tools", role: "developer", tools: [ | ||||||||||||
| { type: "namespace", name: "collaboration", tools: [ | ||||||||||||
| { type: "function", name: "list_agents", description: "a", strict: false, parameters: { type: "object", properties: {}, required: [] } }, | ||||||||||||
| ] }, | ||||||||||||
| { type: "namespace", name: "other__ns", tools: [ | ||||||||||||
| { type: "function", name: "list_agents", description: "b", strict: false, parameters: { type: "object", properties: {}, required: [] } }, | ||||||||||||
| ] }, | ||||||||||||
| ] }, | ||||||||||||
| { type: "message", role: "user", content: [{ type: "input_text", text: "run it" }] }, | ||||||||||||
| ], | ||||||||||||
| } as any); | ||||||||||||
| const maps = buildToolBridgeMaps(parsed as any); | ||||||||||||
| expect(maps.declaredToolNames.has("list_agents")).toBe(false); | ||||||||||||
| expect(maps.toolNsMap.has("list_agents")).toBe(false); | ||||||||||||
| // Both canonical spellings remain declared. | ||||||||||||
| expect(maps.declaredToolNames.has("collaboration__list_agents")).toBe(true); | ||||||||||||
| expect(maps.declaredToolNames.has("other__ns__list_agents")).toBe(true); | ||||||||||||
| }); | ||||||||||||
|
|
||||||||||||
| test("a bare name that equals another tool's dotted spelling stays undeclared", () => { | ||||||||||||
| const parsed = parseRequest({ | ||||||||||||
| model: "meta/muse-spark-1.3-contributor", | ||||||||||||
| input: [ | ||||||||||||
| { type: "additional_tools", role: "developer", tools: [ | ||||||||||||
| { type: "namespace", name: "collaboration", tools: [ | ||||||||||||
| { type: "function", name: "list_agents", description: "a", strict: false, parameters: { type: "object", properties: {}, required: [] } }, | ||||||||||||
| ] }, | ||||||||||||
| { type: "namespace", name: "mcp__x", tools: [ | ||||||||||||
| { type: "function", name: "collaboration.list_agents", description: "b", strict: false, parameters: { type: "object", properties: {}, required: [] } }, | ||||||||||||
| ] }, | ||||||||||||
| ] }, | ||||||||||||
| { type: "message", role: "user", content: [{ type: "input_text", text: "run it" }] }, | ||||||||||||
| ], | ||||||||||||
| } as any); | ||||||||||||
| const maps = buildToolBridgeMaps(parsed as any); | ||||||||||||
| // Tool B's bare name ("collaboration.list_agents") collides with tool A's dotted | ||||||||||||
| // spelling, so that bare alias is poisoned; tool A's dotted spelling is poisoned in | ||||||||||||
| // return by the pre-existing dotted rule. Tool B's own distinct dotted alias does not | ||||||||||||
| // collide with anything and stays declared, as do both canonical spellings. | ||||||||||||
| expect(maps.declaredToolNames.has("collaboration.list_agents")).toBe(false); | ||||||||||||
| expect(maps.toolNsMap.has("collaboration.list_agents")).toBe(false); | ||||||||||||
| expect(maps.declaredToolNames.has("mcp__x.collaboration.list_agents")).toBe(true); | ||||||||||||
| expect(maps.toolNsMap.get("mcp__x.collaboration.list_agents")).toEqual({ namespace: "mcp__x", name: "collaboration.list_agents" }); | ||||||||||||
| expect(maps.declaredToolNames.has("collaboration__list_agents")).toBe(true); | ||||||||||||
| expect(maps.declaredToolNames.has("mcp__x__collaboration.list_agents")).toBe(true); | ||||||||||||
| }); | ||||||||||||
|
|
||||||||||||
| test("a bare name that equals another tool's canonical spelling stays undeclared", () => { | ||||||||||||
| const parsed = parseRequest({ | ||||||||||||
| model: "meta/muse-spark-1.3-contributor", | ||||||||||||
| input: [ | ||||||||||||
| { type: "additional_tools", role: "developer", tools: [ | ||||||||||||
| { type: "namespace", name: "collaboration", tools: [ | ||||||||||||
| { type: "function", name: "list_agents", description: "a", strict: false, parameters: { type: "object", properties: {}, required: [] } }, | ||||||||||||
| ] }, | ||||||||||||
| { type: "namespace", name: "mcp__x", tools: [ | ||||||||||||
| { type: "function", name: "collaboration__list_agents", description: "b", strict: false, parameters: { type: "object", properties: {}, required: [] } }, | ||||||||||||
| ] }, | ||||||||||||
| ] }, | ||||||||||||
| { type: "message", role: "user", content: [{ type: "input_text", text: "run it" }] }, | ||||||||||||
| ], | ||||||||||||
| } as any); | ||||||||||||
| const maps = buildToolBridgeMaps(parsed as any); | ||||||||||||
| // Tool B's bare name ("collaboration__list_agents") is also tool A's declared canonical | ||||||||||||
| // spelling, so the bare alias is poisoned. The canonical spelling stays declared — but as | ||||||||||||
| // tool A's wire name, never as an alias of tool B — so assert the identity via toolNsMap. | ||||||||||||
| // Tool B's canonical and dotted spellings remain declared. | ||||||||||||
| expect(maps.toolNsMap.get("collaboration__list_agents")).toEqual({ namespace: "collaboration", name: "list_agents" }); | ||||||||||||
| expect(maps.declaredToolNames.has("mcp__x__collaboration__list_agents")).toBe(true); | ||||||||||||
| expect(maps.declaredToolNames.has("mcp__x.collaboration__list_agents")).toBe(true); | ||||||||||||
| expect(maps.toolNsMap.get("mcp__x.collaboration__list_agents")).toEqual({ namespace: "mcp__x", name: "collaboration__list_agents" }); | ||||||||||||
| }); | ||||||||||||
|
|
||||||||||||
| test("a bare-declared function owns its name and blocks the namespaced tool's bare alias", () => { | ||||||||||||
| const parsed = parseRequest({ | ||||||||||||
| model: "meta/muse-spark-1.3-contributor", | ||||||||||||
| input: [ | ||||||||||||
| { type: "additional_tools", role: "developer", tools: [ | ||||||||||||
| { type: "namespace", name: "collaboration", tools: [ | ||||||||||||
| { type: "function", name: "list_agents", description: "a", strict: false, parameters: { type: "object", properties: {}, required: [] } }, | ||||||||||||
| ] }, | ||||||||||||
| { type: "function", name: "list_agents", description: "b", strict: false, parameters: { type: "object", properties: {}, required: [] } }, | ||||||||||||
| ] }, | ||||||||||||
| { type: "message", role: "user", content: [{ type: "input_text", text: "run it" }] }, | ||||||||||||
| ], | ||||||||||||
| } as any); | ||||||||||||
| const maps = buildToolBridgeMaps(parsed as any); | ||||||||||||
| // The bare-declared (no-namespace) function participates as an owner of "list_agents", | ||||||||||||
| // mirroring the tool_choice bare path's whole-catalog counting, so the namespaced tool | ||||||||||||
| // must not gain it as an echo alias. "list_agents" stays in declaredToolNames because the | ||||||||||||
| // bare function's own wire name IS that spelling; the alias check is toolNsMap, which | ||||||||||||
| // must never map the bare name to the namespaced identity. | ||||||||||||
| expect(maps.toolNsMap.has("list_agents")).toBe(false); | ||||||||||||
| expect(maps.declaredToolNames.has("collaboration__list_agents")).toBe(true); | ||||||||||||
|
Comment on lines
+122
to
+123
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Assert preservation of the bare declaration. This fixture verifies that Add an assertion for Proposed regression assertion expect(maps.toolNsMap.has("list_agents")).toBe(false);
+ expect(maps.declaredToolNames.has("list_agents")).toBe(true);
expect(maps.declaredToolNames.has("collaboration__list_agents")).toBe(true);As per path instructions: “A behavior change in src/ should come with a focused regression test near the existing tests for that subsystem.” 📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Path instructions |
||||||||||||
| }); | ||||||||||||
| }); | ||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add coverage for the canonical-name collision branch. The fixture at
tests/responses/bare-echo-alias.test.ts:58uses Tool B’s bare namecollaboration.list_agents. This matches Tool A’s dotted alias, not its canonical namecollaboration__list_agents. It exercises the dotted-alias collision check inbuildToolBridgeMaps, not the canonical-name check. A regression in the canonical check can therefore pass the current focused tests.Rename this test as the dotted-collision case and assert that Tool B’s distinct dotted alias,
mcp__x.collaboration.list_agents, remains declared. Correct the test comment: the current fixture rejects Tool A’s dotted alias and Tool B’s bare alias, but Tool B’s dotted alias remains declared.Add a separate fixture with Tool B’s name set to
collaboration__list_agents. Because that spelling is also Tool A’s declared canonical name, assert thattoolNsMap.get("collaboration__list_agents")still maps to Tool A rather than relying only ondeclaredToolNames. Also assert that Tool B’s canonical and dotted names remain declared.🤖 Prompt for AI Agents