From 4adddbdab3c5a928ff05fc10ca74ee9f53aeb427 Mon Sep 17 00:00:00 2001 From: huntharo Date: Sun, 15 Mar 2026 13:53:55 -0400 Subject: [PATCH] Plugins: limit binding APIs to Telegram and Discord --- src/plugins/commands.test.ts | 82 ++++++++++++++++++++++++++++++++++++ src/plugins/commands.ts | 14 +++--- 2 files changed, 87 insertions(+), 9 deletions(-) diff --git a/src/plugins/commands.test.ts b/src/plugins/commands.test.ts index de9d92d75c7..e60fdff51fc 100644 --- a/src/plugins/commands.test.ts +++ b/src/plugins/commands.test.ts @@ -2,6 +2,7 @@ import { afterEach, describe, expect, it } from "vitest"; import { __testing, clearPluginCommands, + executePluginCommand, getPluginCommandSpecs, listPluginCommands, registerPluginCommand, @@ -94,6 +95,7 @@ describe("registerPluginCommand", () => { acceptsArgs: false, }, ]); + expect(getPluginCommandSpecs("slack")).toEqual([]); }); it("resolves Discord DM command bindings with the user target prefix intact", () => { @@ -124,4 +126,84 @@ describe("registerPluginCommand", () => { conversationId: "channel:1480554272859881494", }); }); + + it("does not resolve binding conversations for unsupported command channels", () => { + expect( + __testing.resolveBindingConversationFromCommand({ + channel: "slack", + from: "slack:U123", + to: "C456", + accountId: "default", + }), + ).toBeNull(); + }); + + it("does not expose binding APIs to plugin commands on unsupported channels", async () => { + registerPluginCommand( + "demo-plugin", + { + name: "bindcheck", + description: "Demo command", + acceptsArgs: false, + handler: async (ctx) => { + const requested = await ctx.requestConversationBinding({ + summary: "Bind this conversation.", + }); + const current = await ctx.getCurrentConversationBinding(); + const detached = await ctx.detachConversationBinding(); + return { + text: JSON.stringify({ + requested, + current, + detached, + }), + }; + }, + }, + { pluginRoot: "/plugins/demo-plugin" }, + ); + + const result = await executePluginCommand({ + command: { + name: "bindcheck", + description: "Demo command", + acceptsArgs: false, + handler: async (ctx) => { + const requested = await ctx.requestConversationBinding({ + summary: "Bind this conversation.", + }); + const current = await ctx.getCurrentConversationBinding(); + const detached = await ctx.detachConversationBinding(); + return { + text: JSON.stringify({ + requested, + current, + detached, + }), + }; + }, + pluginId: "demo-plugin", + pluginRoot: "/plugins/demo-plugin", + }, + channel: "slack", + senderId: "U123", + isAuthorizedSender: true, + commandBody: "/bindcheck", + config: {} as never, + from: "slack:U123", + to: "C456", + accountId: "default", + }); + + expect(result.text).toBe( + JSON.stringify({ + requested: { + status: "error", + message: "This command cannot bind the current conversation.", + }, + current: null, + detached: { removed: false }, + }), + ); + }); }); diff --git a/src/plugins/commands.ts b/src/plugins/commands.ts index a0313fee70b..a6c3b5c9b66 100644 --- a/src/plugins/commands.ts +++ b/src/plugins/commands.ts @@ -306,15 +306,7 @@ function resolveBindingConversationFromCommand(params: { conversationId: `${target.kind}:${target.id}`, }; } - const rawTarget = params.to ?? params.from; - if (!rawTarget) { - return null; - } - return { - channel: params.channel, - accountId, - conversationId: rawTarget, - }; + return null; } /** @@ -464,6 +456,10 @@ export function getPluginCommandSpecs(provider?: string): Array<{ description: string; acceptsArgs: boolean; }> { + const providerName = provider?.trim().toLowerCase(); + if (providerName && providerName !== "telegram" && providerName !== "discord") { + return []; + } return Array.from(pluginCommands.values()).map((cmd) => ({ name: resolvePluginNativeName(cmd, provider), description: cmd.description,