refactor: share dual text command gating

This commit is contained in:
Peter Steinberger 2026-03-14 01:23:36 +00:00
parent 91d9573b55
commit b61bc4948e
5 changed files with 55 additions and 23 deletions

View File

@ -9,7 +9,7 @@ import {
evaluateSenderGroupAccessForPolicy, evaluateSenderGroupAccessForPolicy,
resolveSenderScopedGroupPolicy, resolveSenderScopedGroupPolicy,
recordPendingHistoryEntryIfEnabled, recordPendingHistoryEntryIfEnabled,
resolveControlCommandGate, resolveDualTextControlCommandGate,
resolveDefaultGroupPolicy, resolveDefaultGroupPolicy,
isDangerousNameMatchingEnabled, isDangerousNameMatchingEnabled,
readStoreAllowFromForDmPolicy, readStoreAllowFromForDmPolicy,
@ -297,18 +297,15 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
senderName, senderName,
allowNameMatching: isDangerousNameMatchingEnabled(msteamsCfg), allowNameMatching: isDangerousNameMatchingEnabled(msteamsCfg),
}); });
const hasControlCommandInMessage = core.channel.text.hasControlCommand(text, cfg); const { commandAuthorized, shouldBlock } = resolveDualTextControlCommandGate({
const commandGate = resolveControlCommandGate({
useAccessGroups, useAccessGroups,
authorizers: [ primaryConfigured: commandDmAllowFrom.length > 0,
{ configured: commandDmAllowFrom.length > 0, allowed: ownerAllowedForCommands }, primaryAllowed: ownerAllowedForCommands,
{ configured: effectiveGroupAllowFrom.length > 0, allowed: groupAllowedForCommands }, secondaryConfigured: effectiveGroupAllowFrom.length > 0,
], secondaryAllowed: groupAllowedForCommands,
allowTextCommands: true, hasControlCommand: core.channel.text.hasControlCommand(text, cfg),
hasControlCommand: hasControlCommandInMessage,
}); });
const commandAuthorized = commandGate.commandAuthorized; if (shouldBlock) {
if (commandGate.shouldBlock) {
logInboundDrop({ logInboundDrop({
log: logVerboseMessage, log: logVerboseMessage,
channel: "msteams", channel: "msteams",

View File

@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import { import {
resolveCommandAuthorizedFromAuthorizers, resolveCommandAuthorizedFromAuthorizers,
resolveControlCommandGate, resolveControlCommandGate,
resolveDualTextControlCommandGate,
} from "./command-gating.js"; } from "./command-gating.js";
describe("resolveCommandAuthorizedFromAuthorizers", () => { describe("resolveCommandAuthorizedFromAuthorizers", () => {
@ -94,4 +95,17 @@ describe("resolveControlCommandGate", () => {
}); });
expect(result.shouldBlock).toBe(false); expect(result.shouldBlock).toBe(false);
}); });
it("supports the dual-authorizer text gate helper", () => {
const result = resolveDualTextControlCommandGate({
useAccessGroups: true,
primaryConfigured: true,
primaryAllowed: false,
secondaryConfigured: true,
secondaryAllowed: true,
hasControlCommand: true,
});
expect(result.commandAuthorized).toBe(true);
expect(result.shouldBlock).toBe(false);
});
}); });

View File

@ -43,3 +43,24 @@ export function resolveControlCommandGate(params: {
const shouldBlock = params.allowTextCommands && params.hasControlCommand && !commandAuthorized; const shouldBlock = params.allowTextCommands && params.hasControlCommand && !commandAuthorized;
return { commandAuthorized, shouldBlock }; return { commandAuthorized, shouldBlock };
} }
export function resolveDualTextControlCommandGate(params: {
useAccessGroups: boolean;
primaryConfigured: boolean;
primaryAllowed: boolean;
secondaryConfigured: boolean;
secondaryAllowed: boolean;
hasControlCommand: boolean;
modeWhenAccessGroupsOff?: CommandGatingModeWhenAccessGroupsOff;
}): { commandAuthorized: boolean; shouldBlock: boolean } {
return resolveControlCommandGate({
useAccessGroups: params.useAccessGroups,
authorizers: [
{ configured: params.primaryConfigured, allowed: params.primaryAllowed },
{ configured: params.secondaryConfigured, allowed: params.secondaryAllowed },
],
allowTextCommands: true,
hasControlCommand: params.hasControlCommand,
modeWhenAccessGroupsOff: params.modeWhenAccessGroupsOff,
});
}

View File

@ -12,7 +12,7 @@ import {
} from "../../auto-reply/reply/history.js"; } from "../../auto-reply/reply/history.js";
import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js"; import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
import { buildMentionRegexes, matchesMentionPatterns } from "../../auto-reply/reply/mentions.js"; import { buildMentionRegexes, matchesMentionPatterns } from "../../auto-reply/reply/mentions.js";
import { resolveControlCommandGate } from "../../channels/command-gating.js"; import { resolveDualTextControlCommandGate } from "../../channels/command-gating.js";
import { logInboundDrop } from "../../channels/logging.js"; import { logInboundDrop } from "../../channels/logging.js";
import type { OpenClawConfig } from "../../config/config.js"; import type { OpenClawConfig } from "../../config/config.js";
import { import {
@ -310,18 +310,15 @@ export function resolveIMessageInboundDecision(params: {
chatIdentifier, chatIdentifier,
}) })
: false; : false;
const hasControlCommandInMessage = hasControlCommand(messageText, params.cfg); const { commandAuthorized, shouldBlock } = resolveDualTextControlCommandGate({
const commandGate = resolveControlCommandGate({
useAccessGroups, useAccessGroups,
authorizers: [ primaryConfigured: commandDmAllowFrom.length > 0,
{ configured: commandDmAllowFrom.length > 0, allowed: ownerAllowedForCommands }, primaryAllowed: ownerAllowedForCommands,
{ configured: effectiveGroupAllowFrom.length > 0, allowed: groupAllowedForCommands }, secondaryConfigured: effectiveGroupAllowFrom.length > 0,
], secondaryAllowed: groupAllowedForCommands,
allowTextCommands: true, hasControlCommand: hasControlCommand(messageText, params.cfg),
hasControlCommand: hasControlCommandInMessage,
}); });
const commandAuthorized = commandGate.commandAuthorized; if (isGroup && shouldBlock) {
if (isGroup && commandGate.shouldBlock) {
if (params.logVerbose) { if (params.logVerbose) {
logInboundDrop({ logInboundDrop({
log: params.logVerbose, log: params.logVerbose,

View File

@ -12,7 +12,10 @@ export {
export { isSilentReplyText, SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js"; export { isSilentReplyText, SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
export type { ReplyPayload } from "../auto-reply/types.js"; export type { ReplyPayload } from "../auto-reply/types.js";
export { mergeAllowlist, summarizeMapping } from "../channels/allowlists/resolve-utils.js"; export { mergeAllowlist, summarizeMapping } from "../channels/allowlists/resolve-utils.js";
export { resolveControlCommandGate } from "../channels/command-gating.js"; export {
resolveControlCommandGate,
resolveDualTextControlCommandGate,
} from "../channels/command-gating.js";
export { logInboundDrop, logTypingFailure } from "../channels/logging.js"; export { logInboundDrop, logTypingFailure } from "../channels/logging.js";
export { resolveMentionGating } from "../channels/mention-gating.js"; export { resolveMentionGating } from "../channels/mention-gating.js";
export type { AllowlistMatch } from "../channels/plugins/allowlist-match.js"; export type { AllowlistMatch } from "../channels/plugins/allowlist-match.js";