Skip to content

Commit 71afa5f

Browse files
committed
fix(responses): honor namespace tool choice
1 parent c44e43f commit 71afa5f

2 files changed

Lines changed: 66 additions & 1 deletion

File tree

‎src/responses/namespace-tool-compat.ts‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,38 @@ function rewriteToolChoice(value: unknown, plan: NamespaceRewritePlan): unknown
239239
return changed ? { ...value, tools } : value;
240240
}
241241

242+
/**
243+
* Keep response restoration inside the caller's per-turn tool authorization boundary. The
244+
* upstream sees every flattened declaration even when `tool_choice` narrows the tools it may call,
245+
* so its output cannot be trusted merely because a wire name appeared in that catalog.
246+
*/
247+
function authorizedAliases(
248+
aliases: Map<string, RoutedNamespaceToolIdentity>,
249+
toolChoice: unknown,
250+
): Map<string, RoutedNamespaceToolIdentity> {
251+
if (toolChoice === undefined || toolChoice === "auto" || toolChoice === "required") return aliases;
252+
if (toolChoice === "none" || !isPlainObject(toolChoice)) return new Map();
253+
254+
let authorizedNames: Set<string>;
255+
if (
256+
(toolChoice.type === "function" || toolChoice.type === "custom")
257+
&& typeof toolChoice.name === "string"
258+
) {
259+
authorizedNames = new Set([toolChoice.name]);
260+
} else if (toolChoice.type === "allowed_tools" && Array.isArray(toolChoice.tools)) {
261+
authorizedNames = new Set(
262+
toolChoice.tools
263+
.filter(tool => isPlainObject(tool) && typeof tool.name === "string")
264+
.map(tool => tool.name as string),
265+
);
266+
} else {
267+
// An explicit selector for another tool kind does not authorize a client namespace call.
268+
return new Map();
269+
}
270+
271+
return new Map([...aliases].filter(([wireName]) => authorizedNames.has(wireName)));
272+
}
273+
242274
function rewriteInputItem(item: unknown, plan: NamespaceRewritePlan, emitted: Set<string>): unknown {
243275
if (!isPlainObject(item)) return item;
244276
if (item.type === "additional_tools" && Array.isArray(item.tools)) {
@@ -292,7 +324,7 @@ export function rewriteRoutedNamespaceToolsForUpstream(body: unknown): {
292324
...(input !== body.input ? { input } : {}),
293325
...(toolChoice !== body.tool_choice ? { tool_choice: toolChoice } : {}),
294326
},
295-
aliases: plan.aliases,
327+
aliases: authorizedAliases(plan.aliases, toolChoice),
296328
};
297329
}
298330

‎tests/namespace-tool-compat.test.ts‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,39 @@ describe("Responses namespace tool compatibility", () => {
104104
expect(directCollision.tool_choice.name).toBe("read");
105105
});
106106

107+
test("only arms response aliases authorized by tool_choice", () => {
108+
const tools = [{
109+
type: "namespace",
110+
name: "collaboration",
111+
tools: [
112+
{ type: "function", name: "safe" },
113+
{ type: "function", name: "excluded" },
114+
],
115+
}];
116+
117+
const allowed = rewriteRoutedNamespaceToolsForUpstream({
118+
tools,
119+
tool_choice: {
120+
type: "allowed_tools",
121+
mode: "required",
122+
tools: [{ type: "function", namespace: "collaboration", name: "safe" }],
123+
},
124+
});
125+
expect([...allowed.aliases]).toEqual([
126+
["collaboration__safe", { namespace: "collaboration", name: "safe" }],
127+
]);
128+
expect(restoreRoutedNamespaceCalls({
129+
type: "function_call",
130+
name: "collaboration__excluded",
131+
}, allowed.aliases).changed).toBe(false);
132+
133+
expect(rewriteRoutedNamespaceToolsForUpstream({
134+
tools,
135+
tool_choice: { type: "function", namespace: "collaboration", name: "safe" },
136+
}).aliases.has("collaboration__excluded")).toBe(false);
137+
expect(rewriteRoutedNamespaceToolsForUpstream({ tools, tool_choice: "none" }).aliases.size).toBe(0);
138+
});
139+
107140
test("fails closed when flattening would collide with a declared wire name", () => {
108141
expect(() => rewriteRoutedNamespaceToolsForUpstream({
109142
tools: [

0 commit comments

Comments
 (0)