fix(actions): layer per-account gate fallback

This commit is contained in:
Sebastian
2026-02-16 20:59:23 -05:00
parent 616c0bd4c7
commit 2b3ecee7c5
10 changed files with 203 additions and 25 deletions

View File

@@ -660,4 +660,50 @@ describe("handleDiscordAction per-account gating", () => {
);
expect(kickMemberDiscord).toHaveBeenCalled();
});
it("inherits top-level channel gate when account overrides moderation only", async () => {
const cfg = {
channels: {
discord: {
actions: { channels: false },
accounts: {
ops: { token: "tok-ops", actions: { moderation: true } },
},
},
},
} as OpenClawConfig;
await expect(
handleDiscordAction(
{ action: "channelCreate", guildId: "G1", name: "alerts", accountId: "ops" },
cfg,
),
).rejects.toThrow(/channel management is disabled/i);
});
it("allows account to explicitly re-enable top-level disabled channel gate", async () => {
const cfg = {
channels: {
discord: {
actions: { channels: false },
accounts: {
ops: {
token: "tok-ops",
actions: { moderation: true, channels: true },
},
},
},
},
} as OpenClawConfig;
await handleDiscordAction(
{ action: "channelCreate", guildId: "G1", name: "alerts", accountId: "ops" },
cfg,
);
expect(createChannelDiscord).toHaveBeenCalledWith(
expect.objectContaining({ guildId: "G1", name: "alerts" }),
{ accountId: "ops" },
);
});
});

View File

@@ -1,7 +1,7 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import type { OpenClawConfig } from "../../config/config.js";
import { resolveDiscordAccount } from "../../discord/accounts.js";
import { createActionGate, readStringParam } from "./common.js";
import { createDiscordActionGate } from "../../discord/accounts.js";
import { readStringParam } from "./common.js";
import { handleDiscordGuildAction } from "./discord-actions-guild.js";
import { handleDiscordMessagingAction } from "./discord-actions-messaging.js";
import { handleDiscordModerationAction } from "./discord-actions-moderation.js";
@@ -61,8 +61,7 @@ export async function handleDiscordAction(
): Promise<AgentToolResult<unknown>> {
const action = readStringParam(params, "action", { required: true });
const accountId = readStringParam(params, "accountId");
const account = resolveDiscordAccount({ cfg, accountId });
const isActionEnabled = createActionGate(account.config.actions);
const isActionEnabled = createDiscordActionGate({ cfg, accountId });
if (messagingActions.has(action)) {
return await handleDiscordMessagingAction(action, params, isActionEnabled);

View File

@@ -686,4 +686,61 @@ describe("handleTelegramAction per-account gating", () => {
expect.objectContaining({ token: "tok-media" }),
);
});
it("inherits top-level reaction gate when account overrides sticker only", async () => {
const cfg = {
channels: {
telegram: {
actions: { reactions: false },
accounts: {
media: { botToken: "tok-media", actions: { sticker: true } },
},
},
},
} as OpenClawConfig;
await expect(
handleTelegramAction(
{
action: "react",
chatId: "123",
messageId: 1,
emoji: "👀",
accountId: "media",
},
cfg,
),
).rejects.toThrow(/reactions are disabled via actions.reactions/i);
});
it("allows account to explicitly re-enable top-level disabled reaction gate", async () => {
const cfg = {
channels: {
telegram: {
actions: { reactions: false },
accounts: {
media: { botToken: "tok-media", actions: { sticker: true, reactions: true } },
},
},
},
} as OpenClawConfig;
await handleTelegramAction(
{
action: "react",
chatId: "123",
messageId: 1,
emoji: "👀",
accountId: "media",
},
cfg,
);
expect(reactMessageTelegram).toHaveBeenCalledWith(
"123",
1,
"👀",
expect.objectContaining({ token: "tok-media", accountId: "media" }),
);
});
});

View File

@@ -1,7 +1,7 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import type { OpenClawConfig } from "../../config/config.js";
import { resolveTelegramAccount } from "../../telegram/accounts.js";
import type { TelegramButtonStyle, TelegramInlineButtons } from "../../telegram/button-types.js";
import { createTelegramActionGate } from "../../telegram/accounts.js";
import {
resolveTelegramInlineButtonsScope,
resolveTelegramTargetChatType,
@@ -18,7 +18,6 @@ import {
import { getCacheStats, searchStickers } from "../../telegram/sticker-cache.js";
import { resolveTelegramToken } from "../../telegram/token.js";
import {
createActionGate,
jsonResult,
readNumberParam,
readReactionParams,
@@ -89,8 +88,7 @@ export async function handleTelegramAction(
): Promise<AgentToolResult<unknown>> {
const action = readStringParam(params, "action", { required: true });
const accountId = readStringParam(params, "accountId");
const account = resolveTelegramAccount({ cfg, accountId });
const isActionEnabled = createActionGate(account.config.actions);
const isActionEnabled = createTelegramActionGate({ cfg, accountId });
if (action === "react") {
// Check reaction level first