diff --git a/src/agents/pi-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping.test.ts b/src/agents/pi-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping.test.ts index e074b6f9189..22d68f15ff8 100644 --- a/src/agents/pi-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping.test.ts +++ b/src/agents/pi-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping.test.ts @@ -319,19 +319,6 @@ describe("createOpenClawCodingTools", () => { expect(names.has("telegram")).toBe(false); expect(names.has("whatsapp")).toBe(false); }); - it.each(["voice", "VOICE", " Voice "])( - "does not expose tts tool for normalized voice message provider: %s", - (messageProvider) => { - const tools = createOpenClawCodingTools({ messageProvider }); - const names = new Set(tools.map((tool) => tool.name)); - expect(names.has("tts")).toBe(false); - }, - ); - it("keeps tts tool for non-voice providers", () => { - const tools = createOpenClawCodingTools({ messageProvider: "discord" }); - const names = new Set(tools.map((tool) => tool.name)); - expect(names.has("tts")).toBe(true); - }); it("filters session tools for sub-agent sessions by default", () => { const tools = createOpenClawCodingTools({ sessionKey: "agent:main:subagent:test", diff --git a/src/agents/pi-tools.message-provider-policy.test.ts b/src/agents/pi-tools.message-provider-policy.test.ts new file mode 100644 index 00000000000..0bcdd5144f0 --- /dev/null +++ b/src/agents/pi-tools.message-provider-policy.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; +import { createOpenClawCodingTools } from "./pi-tools.js"; + +describe("createOpenClawCodingTools message provider policy", () => { + it.each(["voice", "VOICE", " Voice "])( + "does not expose tts tool for normalized voice provider: %s", + (messageProvider) => { + const tools = createOpenClawCodingTools({ messageProvider }); + const names = new Set(tools.map((tool) => tool.name)); + expect(names.has("tts")).toBe(false); + }, + ); + + it("keeps tts tool for non-voice providers", () => { + const tools = createOpenClawCodingTools({ messageProvider: "discord" }); + const names = new Set(tools.map((tool) => tool.name)); + expect(names.has("tts")).toBe(true); + }); +}); diff --git a/src/agents/pi-tools.ts b/src/agents/pi-tools.ts index f4252f562bb..15be5766c89 100644 --- a/src/agents/pi-tools.ts +++ b/src/agents/pi-tools.ts @@ -67,6 +67,31 @@ function isOpenAIProvider(provider?: string) { return normalized === "openai" || normalized === "openai-codex"; } +const TOOL_DENY_BY_MESSAGE_PROVIDER: Readonly> = { + voice: ["tts"], +}; + +function normalizeMessageProvider(messageProvider?: string): string | undefined { + const normalized = messageProvider?.trim().toLowerCase(); + return normalized && normalized.length > 0 ? normalized : undefined; +} + +function applyMessageProviderToolPolicy( + tools: AnyAgentTool[], + messageProvider?: string, +): AnyAgentTool[] { + const normalizedProvider = normalizeMessageProvider(messageProvider); + if (!normalizedProvider) { + return tools; + } + const deniedTools = TOOL_DENY_BY_MESSAGE_PROVIDER[normalizedProvider]; + if (!deniedTools || deniedTools.length === 0) { + return tools; + } + const deniedSet = new Set(deniedTools); + return tools.filter((tool) => !deniedSet.has(tool.name)); +} + function isApplyPatchAllowedForModel(params: { modelProvider?: string; modelId?: string; @@ -217,8 +242,6 @@ export function createOpenClawCodingTools(options?: { /** Whether the sender is an owner (required for owner-only tools). */ senderIsOwner?: boolean; }): AnyAgentTool[] { - const rawMessageProvider = options?.messageProvider?.trim().toLowerCase(); - const isVoiceMessageProvider = rawMessageProvider === "voice"; const execToolName = "exec"; const sandbox = options?.sandbox?.enabled ? options.sandbox : undefined; const { @@ -482,9 +505,7 @@ export function createOpenClawCodingTools(options?: { senderIsOwner: options?.senderIsOwner, }), ]; - const toolsForMessageProvider = isVoiceMessageProvider - ? tools.filter((tool) => tool.name !== "tts") - : tools; + const toolsForMessageProvider = applyMessageProviderToolPolicy(tools, options?.messageProvider); // Security: treat unknown/undefined as unauthorized (opt-in, not opt-out) const senderIsOwner = options?.senderIsOwner === true; const toolsByAuthorization = applyOwnerOnlyToolPolicy(toolsForMessageProvider, senderIsOwner);