diff --git a/src/auto-reply/command-auth.ts b/src/auto-reply/command-auth.ts index c1885d3f9553..d544c384d36a 100644 --- a/src/auto-reply/command-auth.ts +++ b/src/auto-reply/command-auth.ts @@ -1,6 +1,5 @@ /** Command authorization helpers for owner and allowlist checks. */ import { - normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, } from "@openclaw/normalization-core/string-coerce"; @@ -19,6 +18,7 @@ import { normalizeMessageChannel, } from "../utils/message-channel.js"; import { isNativeCommandTurn, resolveCommandTurnContext } from "./command-turn-context.js"; +import { shouldUseFromAsSenderFallback } from "./sender-identity.js"; import type { MsgContext } from "./templating.js"; export type CommandAuthorization = { @@ -461,32 +461,6 @@ function resolveCommandSenderAuthorization(params: { return params.commandAuthorized && (params.isOwnerForCommands || params.nativeCommandAuthorized); } -function isConversationLikeIdentity(value: string): boolean { - const normalized = normalizeOptionalLowercaseString(value); - if (!normalized) { - return false; - } - if (normalized.startsWith("chat_id:")) { - return true; - } - return /(^|:)(channel|group|thread|topic|room|space|spaces):/.test(normalized); -} - -function shouldUseFromAsSenderFallback(params: { - from?: string | null; - chatType?: string | null; -}): boolean { - const from = normalizeOptionalString(params.from) ?? ""; - if (!from) { - return false; - } - const chatType = normalizeLowercaseStringOrEmpty(params.chatType); - if (chatType && chatType !== "direct") { - return false; - } - return !isConversationLikeIdentity(from); -} - function resolveSenderCandidates(params: { plugin?: ChannelPlugin; providerId?: ChannelId; diff --git a/src/auto-reply/reply/reply-elevated.test.ts b/src/auto-reply/reply/reply-elevated.test.ts index a669ed6bf694..30a563ead70e 100644 --- a/src/auto-reply/reply/reply-elevated.test.ts +++ b/src/auto-reply/reply/reply-elevated.test.ts @@ -70,6 +70,32 @@ describe("resolveElevatedPermissions", () => { }); }); + it("does not authorize a group conversation id as a sender identity", () => { + expectAllowFromDecision({ + allowFrom: ["120363411111111111@g.us", "from:120363411111111111@g.us"], + allowed: false, + ctx: { + ChatType: "group", + From: "120363411111111111@g.us", + SenderId: "+15550002222", + SenderE164: "+15550002222", + }, + }); + }); + + it("keeps direct chat From fallback authorization", () => { + expectAllowFromDecision({ + allowFrom: ["from:whatsapp:+15550001111"], + allowed: true, + ctx: { + ChatType: "direct", + From: "whatsapp:+15550001111", + SenderId: undefined, + SenderE164: undefined, + }, + }); + }); + it("does not authorize untyped mutable sender fields", () => { expectAllowFromDecision({ allowFrom: ["owner-display-name"], diff --git a/src/auto-reply/reply/reply-elevated.ts b/src/auto-reply/reply/reply-elevated.ts index 15ed1faafd2d..a1d738cb5054 100644 --- a/src/auto-reply/reply/reply-elevated.ts +++ b/src/auto-reply/reply/reply-elevated.ts @@ -4,6 +4,7 @@ import { normalizeStringEntries } from "@openclaw/normalization-core/string-norm import { resolveAgentConfig } from "../../agents/agent-scope.js"; import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js"; import type { AgentElevatedAllowFromConfig, OpenClawConfig } from "../../config/config.js"; +import { shouldUseFromAsSenderFallback } from "../sender-identity.js"; import type { MsgContext } from "../templating.js"; import { type AllowFromFormatter, @@ -94,7 +95,10 @@ function isApprovedElevatedSender(params: { tokens: senderIdTokens, }); } - if (senderFrom) { + if ( + senderFrom && + shouldUseFromAsSenderFallback({ from: senderFrom, chatType: params.ctx.ChatType }) + ) { addFormattedTokens({ formatAllowFrom: params.formatAllowFrom, values: [senderFrom, stripSenderPrefix(senderFrom)].filter((value): value is string => diff --git a/src/auto-reply/sender-identity.ts b/src/auto-reply/sender-identity.ts new file mode 100644 index 000000000000..0a18f5ba7747 --- /dev/null +++ b/src/auto-reply/sender-identity.ts @@ -0,0 +1,32 @@ +/** Shared sender identity helpers for authorization checks. */ +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalLowercaseString, + normalizeOptionalString, +} from "@openclaw/normalization-core/string-coerce"; + +function isConversationLikeIdentity(value: string): boolean { + const normalized = normalizeOptionalLowercaseString(value); + if (!normalized) { + return false; + } + if (normalized.startsWith("chat_id:")) { + return true; + } + return /(^|:)(channel|group|thread|topic|room|space|spaces):/.test(normalized); +} + +export function shouldUseFromAsSenderFallback(params: { + from?: string | null; + chatType?: string | null; +}): boolean { + const from = normalizeOptionalString(params.from) ?? ""; + if (!from) { + return false; + } + const chatType = normalizeLowercaseStringOrEmpty(params.chatType); + if (chatType && chatType !== "direct") { + return false; + } + return !isConversationLikeIdentity(from); +}