Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion src/responses/namespace-tool-compat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,38 @@ function rewriteToolChoice(value: unknown, plan: NamespaceRewritePlan): unknown
return changed ? { ...value, tools } : value;
}

/**
* Keep response restoration inside the caller's per-turn tool authorization boundary. The
* upstream sees every flattened declaration even when `tool_choice` narrows the tools it may call,
* so its output cannot be trusted merely because a wire name appeared in that catalog.
*/
function authorizedAliases(
aliases: Map<string, RoutedNamespaceToolIdentity>,
toolChoice: unknown,
): Map<string, RoutedNamespaceToolIdentity> {
if (toolChoice === undefined || toolChoice === "auto" || toolChoice === "required") return aliases;
if (toolChoice === "none" || !isPlainObject(toolChoice)) return new Map();

let authorizedNames: Set<string>;
if (
(toolChoice.type === "function" || toolChoice.type === "custom")
&& typeof toolChoice.name === "string"
) {
authorizedNames = new Set([toolChoice.name]);
} else if (toolChoice.type === "allowed_tools" && Array.isArray(toolChoice.tools)) {
authorizedNames = new Set(
toolChoice.tools
.filter(tool => isPlainObject(tool) && typeof tool.name === "string")
.map(tool => tool.name as string),
);
} else {
// An explicit selector for another tool kind does not authorize a client namespace call.
return new Map();
}

return new Map([...aliases].filter(([wireName]) => authorizedNames.has(wireName)));
}

function rewriteInputItem(item: unknown, plan: NamespaceRewritePlan, emitted: Set<string>): unknown {
if (!isPlainObject(item)) return item;
if (item.type === "additional_tools" && Array.isArray(item.tools)) {
Expand Down Expand Up @@ -292,7 +324,7 @@ export function rewriteRoutedNamespaceToolsForUpstream(body: unknown): {
...(input !== body.input ? { input } : {}),
...(toolChoice !== body.tool_choice ? { tool_choice: toolChoice } : {}),
},
aliases: plan.aliases,
aliases: authorizedAliases(plan.aliases, toolChoice),
};
}

Expand Down
33 changes: 33 additions & 0 deletions tests/namespace-tool-compat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,39 @@ describe("Responses namespace tool compatibility", () => {
expect(directCollision.tool_choice.name).toBe("read");
});

test("only arms response aliases authorized by tool_choice", () => {
const tools = [{
type: "namespace",
name: "collaboration",
tools: [
{ type: "function", name: "safe" },
{ type: "function", name: "excluded" },
],
}];

const allowed = rewriteRoutedNamespaceToolsForUpstream({
tools,
tool_choice: {
type: "allowed_tools",
mode: "required",
tools: [{ type: "function", namespace: "collaboration", name: "safe" }],
},
});
expect([...allowed.aliases]).toEqual([
["collaboration__safe", { namespace: "collaboration", name: "safe" }],
]);
expect(restoreRoutedNamespaceCalls({
type: "function_call",
name: "collaboration__excluded",
}, allowed.aliases).changed).toBe(false);

expect(rewriteRoutedNamespaceToolsForUpstream({
tools,
tool_choice: { type: "function", namespace: "collaboration", name: "safe" },
}).aliases.has("collaboration__excluded")).toBe(false);
expect(rewriteRoutedNamespaceToolsForUpstream({ tools, tool_choice: "none" }).aliases.size).toBe(0);
});

test("fails closed when flattening would collide with a declared wire name", () => {
expect(() => rewriteRoutedNamespaceToolsForUpstream({
tools: [
Expand Down
Loading