From 4fe64a0a79b9174c8c813e5b569a15c1e87d2e91 Mon Sep 17 00:00:00 2001 From: Hulian Felipe Muller Buligon Date: Tue, 15 Sep 2026 22:36:51 -0300 Subject: [PATCH 1/8] fix: accept unambiguous bare tool-name echoes for namespaced tools (#4679) Providers in the muse family (Command Code route included) sometimes echo a namespaced tool call by its bare name - spawn_agent, list_agents, exec - instead of the declared __ spelling. The fail-closed undeclared-tool guard then kills the stream mid-turn and the client re-sends the entire turn. buildToolBridgeMaps now also registers a namespaced tool's BARE name as an echo alias (declaredToolNames + toolNsMap + toolParameterSchemas) when exactly one authorized identity claims it, mirroring the dotted alias handling: bare names claimed by two identities, or ones equal to another tool's canonical/dotted spelling, stay poisoned and undeclared. Bare-declared (no-namespace) functions participate as owners too, aligning with the tool_choice bareNameCounts rule. The restore-path contract comment (passthrough-dispatch.ts, moved from core.ts) now distinguishes ordinary bare aliases from the exec exception. --- src/server/responses/collaboration.ts | 49 ++++++++ src/server/responses/passthrough-dispatch.ts | 11 +- tests/responses/bare-echo-alias.test.ts | 125 +++++++++++++++++++ 3 files changed, 181 insertions(+), 4 deletions(-) create mode 100644 tests/responses/bare-echo-alias.test.ts diff --git a/src/server/responses/collaboration.ts b/src/server/responses/collaboration.ts index 60b6661936..c51db89c5b 100644 --- a/src/server/responses/collaboration.ts +++ b/src/server/responses/collaboration.ts @@ -151,6 +151,38 @@ export function buildToolBridgeMaps(parsed: OcxParsedRequest, budget?: Translato dottedAliasOwners.set(t.name, null); } } + // Bare echo alias (`name` with no namespace spelling, #4679): some providers — observed + // on the muse family via Command Code — echo a namespaced tool by its bare name. The + // bare spelling is only a safe alias while it names ONE tool and cannot be read as + // another identity's canonical or dotted spelling. + // Code-mode helper spellings never gain a bare alias (#4679 review): admitting bare + // `exec` into the declared set would authorize the unrelated helper normalization that + // the CODE_MODE_EXEC exception exists to contain. + const BARE_ECHO_EXCLUDED_NAMES = new Set([ + "exec", "exec_command", "shell_command", "write_stdin", "apply_patch", "view_image", + ]); + const bareAliasOwners = new Map(); + for (const t of authorizedTools) { + // Bare (no-namespace) declarations participate as owners too: a namespaced tool whose + // bare name equals a bare-declared function must not gain the bare alias, mirroring how + // the tool_choice bare path refuses ambiguous owners across the whole request catalog. + const identity = JSON.stringify([t.namespace ?? null, t.name]); + const owner = bareAliasOwners.get(t.name); + if (owner === undefined) bareAliasOwners.set(t.name, identity); + else if (owner !== identity) bareAliasOwners.set(t.name, null); + } + for (const t of authorizedTools) { + const canonical = namespacedToolName(t.namespace, t.name); + const owner = bareAliasOwners.get(canonical); + if (owner !== undefined && owner !== JSON.stringify([t.namespace, t.name])) { + bareAliasOwners.set(canonical, null); + } + const dotted = dottedToolName(t.namespace, t.name); + const dottedOwner = bareAliasOwners.get(dotted); + if (dottedOwner !== undefined && dottedOwner !== JSON.stringify([t.namespace, t.name])) { + bareAliasOwners.set(dotted, null); + } + } for (const t of authorizedTools) { // Upstream output is untrusted: only restore calls for tools the caller authorized. const wireName = namespacedToolName(t.namespace, t.name); @@ -174,6 +206,23 @@ export function buildToolBridgeMaps(parsed: OcxParsedRequest, budget?: Translato toolNsMap.set(dottedName, { namespace: t.namespace, name: t.name, ...(t.freeform ? { freeform: true } : {}) }); if (t.parameters && typeof t.parameters === "object") toolParameterSchemas.set(dottedName, t.parameters); } + // Bare echo alias (`name` with no namespace spelling, #4679): same tool identity as + // the flattened wire name, so a provider that drops the namespace prefix still + // restores against this entry. Ambiguous bare names were resolved to null above; + // skipping them falls back to the spellings every provider can still echo. + // Code-mode helper spellings never gain a bare alias: admitting bare `exec` into the + // declared set would let normalizeDeclaredToolName authorize the unrelated helper + // names, the exact surface the CODE_MODE_EXEC exception exists to contain. + if ( + bareAliasOwners.get(t.name) === JSON.stringify([t.namespace, t.name]) + && !BARE_ECHO_EXCLUDED_NAMES.has(t.name) + ) { + budget?.chargeRetained(new TextEncoder().encode(t.name).byteLength, { kind: "retained_collectors" }); + declaredToolNames.add(t.name); + budget?.chargeRetained(new TextEncoder().encode(JSON.stringify([t.name, t.namespace, t.name])).byteLength, { kind: "retained_collectors" }); + toolNsMap.set(t.name, { namespace: t.namespace, name: t.name, ...(t.freeform ? { freeform: true } : {}) }); + if (t.parameters && typeof t.parameters === "object") toolParameterSchemas.set(t.name, t.parameters); + } } if (t.freeform) { budget?.chargeRetained(new TextEncoder().encode(t.name).byteLength, { kind: "retained_collectors" }); diff --git a/src/server/responses/passthrough-dispatch.ts b/src/server/responses/passthrough-dispatch.ts index 274946be98..65eb3512da 100644 --- a/src/server/responses/passthrough-dispatch.ts +++ b/src/server/responses/passthrough-dispatch.ts @@ -348,10 +348,13 @@ export async function preparePassthroughExchange( const declaredWireToolNames = new Set(); const declaredBareWireToolNames = new Set(); const declaredNamelessClientCallTypes = new Set(); - // `buildToolBridgeMaps` creates a bare alias only when the caller selected exactly one - // namespaced tool through a bare tool_choice. Restore that request-bounded identity before - // authorization checks instead of admitting the bare name into the declared set: for `exec`, - // the latter would also authorize the unrelated code-mode helper names. + // `buildToolBridgeMaps` adds each eligible bare alias to `declaredToolNames` and `toolNsMap` + // (one authorized identity claims the bare name). `refreshUndeclaredToolGuard` normally copies + // those entries into `declaredWireToolNames`, but passthrough restoration runs before the + // undeclared-tool guard, so restore that request-bounded identity here, before authorization + // checks. `exec` uses separate handling: its bridge alias is copied into the declared set only + // when the client itself declared bare `exec`, because otherwise code-mode normalization could + // authorize the unrelated code-mode helper names. const authorizedBareNamespaceToolAliases: RoutedNamespaceToolAliases = new Map( [...toolBridgeMaps.toolNsMap].flatMap(([alias, identity]) => alias === identity.name diff --git a/tests/responses/bare-echo-alias.test.ts b/tests/responses/bare-echo-alias.test.ts new file mode 100644 index 0000000000..8b9c973971 --- /dev/null +++ b/tests/responses/bare-echo-alias.test.ts @@ -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); + }); +}); From c47cbda664414d3616b94faeeaa0102eaf203332 Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 16 Sep 2026 15:59:47 +0900 Subject: [PATCH 2/8] fix(tests): register bare-echo-alias.test.ts in the tests/ layout map tests/responses/bare-echo-alias.test.ts, added by the bare-echo alias change, resolves to no domain: its basename does not match any regex seed in scripts/test-layout/layout.json (the responses seed matches apply|chat|citation|continuation|eventstream|legacy|namespace|passthrough| responses|sse|thought|ws prefixes, not bare-), and it has no explicit entry. That fails two root guards that a scoped bun test tests/responses/ run never executes: - tests/test-layout.test.ts:21 reports it in unresolved. - tests/test-layout-tooling.test.ts:247 requires layout.explicit and tests/fixtures/test-layout-expected.json to be identical, and reports the file in unresolvedNew. Add the basename to both tables, as #4774 did for its own new test file. Co-authored-by: Hulian Felipe Muller Buligon --- scripts/test-layout/layout.json | 1 + tests/fixtures/test-layout-expected.json | 1 + 2 files changed, 2 insertions(+) diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index 89d2a010e6..30bcc28b47 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -266,6 +266,7 @@ "autostart-health.test.ts": "service", "azure-adapter.test.ts": "providers", "azure-model-router-tool-schema.test.ts": "providers", + "bare-echo-alias.test.ts": "responses", "baseten-provider.test.ts": "providers", "bearer-admission-routed-provider.test.ts": "codex-integration", "bounded-body.test.ts": "server", diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index 52d44eb91a..5553d5a7e1 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -100,6 +100,7 @@ "autostart-health.test.ts": "service", "azure-adapter.test.ts": "providers", "azure-model-router-tool-schema.test.ts": "providers", + "bare-echo-alias.test.ts": "responses", "baseten-provider.test.ts": "providers", "bearer-admission-routed-provider.test.ts": "codex-integration", "bounded-body.test.ts": "server", From f80cc6057baa304d00a8406d3222e3c4b35257e4 Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 16 Sep 2026 16:18:30 +0900 Subject: [PATCH 3/8] test(responses): keep Code Mode helpers out of bare aliases Lock the existing exec-helper exclusion at the bridge-map boundary so a namespaced helper cannot become an authorized bare echo. Co-authored-by: Hulian Felipe Muller Buligon --- tests/responses/bare-echo-alias.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/responses/bare-echo-alias.test.ts b/tests/responses/bare-echo-alias.test.ts index 8b9c973971..bd5b477895 100644 --- a/tests/responses/bare-echo-alias.test.ts +++ b/tests/responses/bare-echo-alias.test.ts @@ -23,6 +23,13 @@ describe("bare echo alias for namespaced tools (#4679)", () => { expect(maps.toolNsMap.get("list_agents")).toEqual({ namespace: "collaboration", name: "list_agents" }); }); + test("Code Mode helper names never gain a bare alias", () => { + const maps = buildToolBridgeMaps(collabRequest("exec") as any); // justified: parsed fixture matches the request wire shape + expect(maps.declaredToolNames.has("collaboration__exec")).toBe(true); + expect(maps.declaredToolNames.has("exec")).toBe(false); + expect(maps.toolNsMap.has("exec")).toBe(false); + }); + test("a bare name claimed by two namespaces stays undeclared (no hijack)", () => { const parsed = parseRequest({ model: "meta/muse-spark-1.3-contributor", From 0d169c4b9c273952c2fa98bc23c904f538f9a073 Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 16 Sep 2026 16:25:24 +0900 Subject: [PATCH 4/8] test(responses): update parser alias expectations for bare echoes The bare-name carry intentionally adds the unambiguous safe alias to the bridge map. Keep the parser contract fixture aligned with the fail-closed alias map. Co-authored-by: Hulian Felipe Muller Buligon --- tests/responses/responses-parser.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/responses/responses-parser.test.ts b/tests/responses/responses-parser.test.ts index 12ad3d9082..9d2a62458d 100644 --- a/tests/responses/responses-parser.test.ts +++ b/tests/responses/responses-parser.test.ts @@ -193,6 +193,7 @@ describe("Responses parser", () => { expect([...maps.toolNsMap]).toEqual([ ["mcp__tools__safe", { namespace: "mcp__tools", name: "safe" }], ["mcp__tools.safe", { namespace: "mcp__tools", name: "safe" }], + ["safe", { namespace: "mcp__tools", name: "safe" }], ]); expect([...maps.declaredToolNames]).toEqual(["mcp__tools__safe", "mcp__tools.safe", "apply_patch"]); expect([...maps.freeformToolNames]).toEqual(["apply_patch"]); From c4a196a2e1c6e77505dae85706cf6e0d401c4910 Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 16 Sep 2026 16:31:20 +0900 Subject: [PATCH 5/8] test(responses): include the bare alias in declared tool expectations Keep the parser fixture aligned with the bridge map's unambiguous bare-name contract for the safe namespaced function. Co-authored-by: Hulian Felipe Muller Buligon --- tests/responses/responses-parser.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/responses/responses-parser.test.ts b/tests/responses/responses-parser.test.ts index 9d2a62458d..1a78c0d0cf 100644 --- a/tests/responses/responses-parser.test.ts +++ b/tests/responses/responses-parser.test.ts @@ -195,7 +195,7 @@ describe("Responses parser", () => { ["mcp__tools.safe", { namespace: "mcp__tools", name: "safe" }], ["safe", { namespace: "mcp__tools", name: "safe" }], ]); - expect([...maps.declaredToolNames]).toEqual(["mcp__tools__safe", "mcp__tools.safe", "apply_patch"]); + expect([...maps.declaredToolNames]).toEqual(["mcp__tools__safe", "mcp__tools.safe", "safe", "apply_patch"]); expect([...maps.freeformToolNames]).toEqual(["apply_patch"]); expect([...maps.toolSearchToolNames]).toEqual([]); From 56a0c3834ab355edaf552988df42901651625af8 Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 16 Sep 2026 16:33:39 +0900 Subject: [PATCH 6/8] fix(responses): scope helper alias exclusion to collaboration tools A namespaced custom exec from another catalog remains a caller-declared tool. Only collaboration-surface helper spellings are excluded from bare echo aliases, which preserves the existing mcp__functions.exec contract. Co-authored-by: Hulian Felipe Muller Buligon --- src/server/responses/collaboration.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/server/responses/collaboration.ts b/src/server/responses/collaboration.ts index c51db89c5b..0310417485 100644 --- a/src/server/responses/collaboration.ts +++ b/src/server/responses/collaboration.ts @@ -210,12 +210,13 @@ export function buildToolBridgeMaps(parsed: OcxParsedRequest, budget?: Translato // the flattened wire name, so a provider that drops the namespace prefix still // restores against this entry. Ambiguous bare names were resolved to null above; // skipping them falls back to the spellings every provider can still echo. - // Code-mode helper spellings never gain a bare alias: admitting bare `exec` into the - // declared set would let normalizeDeclaredToolName authorize the unrelated helper - // names, the exact surface the CODE_MODE_EXEC exception exists to contain. + // Code-mode helper spellings on the collaboration surface never gain a bare alias: + // admitting bare `exec` there would let normalizeDeclaredToolName authorize unrelated + // helper names. A namespaced custom `exec` from another catalog (for example + // `mcp__functions.exec`) remains an ordinary caller-declared tool. if ( bareAliasOwners.get(t.name) === JSON.stringify([t.namespace, t.name]) - && !BARE_ECHO_EXCLUDED_NAMES.has(t.name) + && !(t.namespace === "collaboration" && BARE_ECHO_EXCLUDED_NAMES.has(t.name)) ) { budget?.chargeRetained(new TextEncoder().encode(t.name).byteLength, { kind: "retained_collectors" }); declaredToolNames.add(t.name); From adee043e2d48d77d0d5190981e93a7cb0761e10a Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 16 Sep 2026 16:39:54 +0900 Subject: [PATCH 7/8] test(responses): cover bare alias in allowed tool selection The selected-tool bridge map retains the unambiguous safe alias, so the allowed_tools parser fixture must assert that alias alongside canonical and dotted spellings. Co-authored-by: Hulian Felipe Muller Buligon --- tests/responses/responses-parser.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/responses/responses-parser.test.ts b/tests/responses/responses-parser.test.ts index 1a78c0d0cf..3c1bc4641a 100644 --- a/tests/responses/responses-parser.test.ts +++ b/tests/responses/responses-parser.test.ts @@ -201,7 +201,7 @@ describe("Responses parser", () => { parsed.options.toolChoice = { allowedTools: ["mcp__tools__safe"], mode: "required" }; maps = buildToolBridgeMaps(parsed); - expect([...maps.toolNsMap.keys()]).toEqual(["mcp__tools__safe", "mcp__tools.safe"]); + expect([...maps.toolNsMap.keys()]).toEqual(["mcp__tools__safe", "mcp__tools.safe", "safe"]); expect([...maps.declaredToolNames]).toEqual(["mcp__tools__safe", "mcp__tools.safe"]); expect([...maps.freeformToolNames]).toEqual([]); From 39d06495cd37cacdb685b844a31c9f168dcb6051 Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 16 Sep 2026 16:46:09 +0900 Subject: [PATCH 8/8] test(responses): include bare alias in selected declared tools The allowed_tools parser branch returns the same safe bare alias in declaredToolNames as in toolNsMap. Co-authored-by: Hulian Felipe Muller Buligon --- tests/responses/responses-parser.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/responses/responses-parser.test.ts b/tests/responses/responses-parser.test.ts index 3c1bc4641a..b42e286c8d 100644 --- a/tests/responses/responses-parser.test.ts +++ b/tests/responses/responses-parser.test.ts @@ -202,7 +202,7 @@ describe("Responses parser", () => { parsed.options.toolChoice = { allowedTools: ["mcp__tools__safe"], mode: "required" }; maps = buildToolBridgeMaps(parsed); expect([...maps.toolNsMap.keys()]).toEqual(["mcp__tools__safe", "mcp__tools.safe", "safe"]); - expect([...maps.declaredToolNames]).toEqual(["mcp__tools__safe", "mcp__tools.safe"]); + expect([...maps.declaredToolNames]).toEqual(["mcp__tools__safe", "mcp__tools.safe", "safe"]); expect([...maps.freeformToolNames]).toEqual([]); parsed.options.toolChoice = { name: "tool_search" };