diff --git a/extensions/zalouser/src/tool.test.ts b/extensions/zalouser/src/tool.test.ts index 59629e53040..d0b8c502a38 100644 --- a/extensions/zalouser/src/tool.test.ts +++ b/extensions/zalouser/src/tool.test.ts @@ -112,6 +112,26 @@ describe("executeZalouserTool", () => { }); }); + it("does not route send actions from foreign ambient thread defaults", async () => { + const tool = createZalouserTool({ + deliveryContext: { + channel: "slack", + to: "channel:C123", + threadId: "1710000000.000100", + }, + }); + + const result = await tool.execute("tool-1", { + action: "send", + message: "hello", + }); + + expect(mockSendMessage).not.toHaveBeenCalled(); + expect(extractDetails(result)).toEqual({ + error: "threadId and message required for send action", + }); + }); + it("returns tool error when send action fails", async () => { mockSendMessage.mockResolvedValueOnce({ ok: false, error: "blocked" }); const result = await executeZalouserTool("tool-1", { diff --git a/extensions/zalouser/src/tool.ts b/extensions/zalouser/src/tool.ts index 2115ff8a393..0c76a355e96 100644 --- a/extensions/zalouser/src/tool.ts +++ b/extensions/zalouser/src/tool.ts @@ -63,15 +63,23 @@ function resolveAmbientZalouserTarget(context?: ZalouserToolContext): { threadId?: string; isGroup?: boolean; } { - const rawTarget = context?.deliveryContext?.to; - if (typeof rawTarget === "string" && rawTarget.trim()) { + const deliveryContext = context?.deliveryContext; + const rawTarget = deliveryContext?.to; + if ( + (deliveryContext?.channel === undefined || deliveryContext.channel === "zalouser") && + typeof rawTarget === "string" && + rawTarget.trim() + ) { try { return parseZalouserOutboundTarget(rawTarget); } catch { // Ignore unrelated delivery targets; explicit tool params still win. } } - const ambientThreadId = context?.deliveryContext?.threadId; + if (deliveryContext?.channel && deliveryContext.channel !== "zalouser") { + return {}; + } + const ambientThreadId = deliveryContext?.threadId; if (typeof ambientThreadId === "string" && ambientThreadId.trim()) { return { threadId: ambientThreadId.trim() }; } diff --git a/src/agents/openclaw-tools.plugin-context.test.ts b/src/agents/openclaw-tools.plugin-context.test.ts index 2db402badd2..38fe4d06c16 100644 --- a/src/agents/openclaw-tools.plugin-context.test.ts +++ b/src/agents/openclaw-tools.plugin-context.test.ts @@ -127,7 +127,7 @@ describe("createOpenClawTools plugin context", () => { ); }); - it("injects ambient thread defaults without mutating shared plugin tool instances", async () => { + it("does not inject ambient thread defaults into plugin tools", async () => { const executeMock = vi.fn(async () => ({ content: [{ type: "text" as const, text: "ok" }], details: {}, @@ -157,18 +157,17 @@ describe("createOpenClawTools plugin context", () => { expect(first).toBeDefined(); expect(second).toBeDefined(); - expect(first).not.toBe(sharedTool); - expect(second).not.toBe(sharedTool); - expect(first).not.toBe(second); + expect(first).toBe(sharedTool); + expect(second).toBe(sharedTool); await first?.execute("call-1", {}); await second?.execute("call-2", {}); - expect(executeMock).toHaveBeenNthCalledWith(1, "call-1", { threadId: "111.222" }); - expect(executeMock).toHaveBeenNthCalledWith(2, "call-2", { threadId: "333.444" }); + expect(executeMock).toHaveBeenNthCalledWith(1, "call-1", {}); + expect(executeMock).toHaveBeenNthCalledWith(2, "call-2", {}); }); - it("injects messageThreadId defaults for missing params objects", async () => { + it("does not inject messageThreadId defaults for missing params objects", async () => { const executeMock = vi.fn(async () => ({ content: [{ type: "text" as const, text: "ok" }], details: {}, @@ -194,10 +193,10 @@ describe("createOpenClawTools plugin context", () => { await wrapped?.execute("call-1", undefined); - expect(executeMock).toHaveBeenCalledWith("call-1", { messageThreadId: 77 }); + expect(executeMock).toHaveBeenCalledWith("call-1", undefined); }); - it("preserves string thread ids for tools that declare string thread parameters", async () => { + it("does not infer string thread ids for tools that declare thread parameters", async () => { const executeMock = vi.fn(async () => ({ content: [{ type: "text" as const, text: "ok" }], details: {}, @@ -223,10 +222,10 @@ describe("createOpenClawTools plugin context", () => { await wrapped?.execute("call-1", {}); - expect(executeMock).toHaveBeenCalledWith("call-1", { threadId: "77" }); + expect(executeMock).toHaveBeenCalledWith("call-1", {}); }); - it("does not override explicit thread params when ambient defaults exist", async () => { + it("preserves explicit thread params when ambient defaults exist", async () => { const executeMock = vi.fn(async () => ({ content: [{ type: "text" as const, text: "ok" }], details: {}, diff --git a/src/agents/plugin-tool-delivery-defaults.ts b/src/agents/plugin-tool-delivery-defaults.ts index 37becdac32a..cea4e71b100 100644 --- a/src/agents/plugin-tool-delivery-defaults.ts +++ b/src/agents/plugin-tool-delivery-defaults.ts @@ -1,145 +1,10 @@ -import { readSnakeCaseParamRaw } from "../param-key.js"; -import { copyPluginToolMeta } from "../plugins/tools.js"; import type { DeliveryContext } from "../utils/delivery-context.js"; import type { AnyAgentTool } from "./tools/common.js"; -type ThreadInjectionKey = "threadId" | "messageThreadId"; - -function coerceAmbientThreadIdForSchema(params: { - value: unknown; - expectedType?: "string" | "number"; -}): string | number | undefined { - const { value, expectedType } = params; - if (value === undefined || value === null) { - return undefined; - } - if (expectedType === "string") { - if (typeof value === "string") { - const trimmed = value.trim(); - return trimmed || undefined; - } - if (typeof value === "number" && Number.isFinite(value)) { - return String(value); - } - return undefined; - } - if (expectedType === "number") { - if (typeof value === "number" && Number.isFinite(value)) { - return value; - } - if (typeof value !== "string") { - return undefined; - } - const trimmed = value.trim(); - if (!trimmed) { - return undefined; - } - const parsed = Number(trimmed); - if (!Number.isFinite(parsed)) { - return undefined; - } - if (/^-?\d+$/.test(trimmed) && !Number.isSafeInteger(parsed)) { - return undefined; - } - return parsed; - } - if (typeof value === "number" && Number.isFinite(value)) { - return value; - } - if (typeof value === "string") { - const trimmed = value.trim(); - return trimmed || undefined; - } - return undefined; -} - -function resolveThreadInjectionTarget(tool: AnyAgentTool): { - key: ThreadInjectionKey; - expectedType?: "string" | "number"; -} | null { - const schema = - tool.parameters && typeof tool.parameters === "object" - ? (tool.parameters as Record) - : null; - const properties = - schema?.properties && typeof schema.properties === "object" - ? (schema.properties as Record) - : null; - if (!properties) { - return null; - } - for (const key of ["threadId", "messageThreadId"] as const) { - const property = - properties[key] && typeof properties[key] === "object" - ? (properties[key] as Record) - : null; - if (!property) { - continue; - } - const type = property.type; - const expectedType = - type === "string" ? "string" : type === "number" || type === "integer" ? "number" : undefined; - return { key, expectedType }; - } - return null; -} - -function wrapPluginToolWithAmbientThreadDefaults(params: { - tool: AnyAgentTool; - ambientThreadId: string | number; -}): AnyAgentTool { - const target = resolveThreadInjectionTarget(params.tool); - if (!params.tool.execute || !target) { - return params.tool; - } - const defaultThreadId = coerceAmbientThreadIdForSchema({ - value: params.ambientThreadId, - expectedType: target.expectedType, - }); - if (defaultThreadId === undefined) { - return params.tool; - } - const originalExecute = params.tool.execute.bind(params.tool); - const wrappedTool: AnyAgentTool = { - ...params.tool, - execute: async (...args: unknown[]) => { - const existingParams = args[1]; - const paramsRecord = - existingParams == null - ? {} - : existingParams && typeof existingParams === "object" && !Array.isArray(existingParams) - ? (existingParams as Record) - : null; - if (!paramsRecord) { - return await originalExecute(...(args as Parameters)); - } - if ( - readSnakeCaseParamRaw(paramsRecord, "threadId") !== undefined || - readSnakeCaseParamRaw(paramsRecord, "messageThreadId") !== undefined - ) { - return await originalExecute(...(args as Parameters)); - } - const nextArgs = [...args]; - nextArgs[1] = { ...paramsRecord, [target.key]: defaultThreadId }; - return await originalExecute(...(nextArgs as Parameters)); - }, - }; - copyPluginToolMeta(params.tool, wrappedTool); - return wrappedTool; -} - export function applyPluginToolDeliveryDefaults(params: { tools: AnyAgentTool[]; deliveryContext?: DeliveryContext; }): AnyAgentTool[] { - const ambientThreadId = params.deliveryContext?.threadId; - if (ambientThreadId == null) { - return params.tools; - } - return params.tools.map((tool) => - wrapPluginToolWithAmbientThreadDefaults({ - tool, - ambientThreadId, - }), - ); + void params.deliveryContext; + return params.tools; }