fix: restore discord owner hint from allowlists

This commit is contained in:
Peter Steinberger
2026-02-04 23:34:08 -08:00
parent 8524666454
commit d84eb46467
8 changed files with 89 additions and 7 deletions

View File

@@ -32,6 +32,7 @@ Docs: https://docs.openclaw.ai
- Web UI: apply button styling to the new-messages indicator.
- Onboarding: infer auth choice from non-interactive API key flags. (#8484) Thanks @f-trycua.
- Security: keep untrusted channel metadata out of system prompts (Slack/Discord). Thanks @KonstantinMirin.
- Discord: treat allowlisted senders as owner for system-prompt identity hints while keeping channel topics untrusted.
- Security: enforce sandboxed media paths for message tool attachments. (#9182) Thanks @victormier.
- Security: require explicit credentials for gateway URL overrides to prevent credential leakage. (#8113) Thanks @victormier.
- Security: gate `whatsapp_login` tool to owner senders and default-deny non-owner contexts. (#8768) Thanks @victormier.

View File

@@ -196,6 +196,7 @@ Notes:
- If `channels` is present, any channel not listed is denied by default.
- Use a `"*"` channel entry to apply defaults across all channels; explicit channel entries override the wildcard.
- Threads inherit parent channel config (allowlist, `requireMention`, skills, prompts, etc.) unless you add the thread channel id explicitly.
- Owner hint: when a per-guild or per-channel `users` allowlist matches the sender, OpenClaw treats that sender as the owner in the system prompt. For a global owner across channels, set `commands.ownerAllowFrom`.
- Bot-authored messages are ignored by default; set `channels.discord.allowBots=true` to allow them (own messages remain filtered).
- Warning: If you allow replies to other bots (`channels.discord.allowBots=true`), prevent bot-to-bot reply loops with `requireMention`, `channels.discord.guilds.*.channels.<id>.users` allowlists, and/or clear guardrails in `AGENTS.md` and `SOUL.md`.
@@ -334,7 +335,7 @@ ack reaction after the bot replies.
- `guilds.<id>.channels.<channel>.toolsBySender`: optional per-sender tool policy overrides within the channel (`"*"` wildcard supported).
- `guilds.<id>.channels.<channel>.users`: optional per-channel user allowlist.
- `guilds.<id>.channels.<channel>.skills`: skill filter (omit = all skills, empty = none).
- `guilds.<id>.channels.<channel>.systemPrompt`: extra system prompt for the channel (combined with channel topic).
- `guilds.<id>.channels.<channel>.systemPrompt`: extra system prompt for the channel. Discord channel topics are injected as **untrusted** context (not system prompt).
- `guilds.<id>.channels.<channel>.enabled`: set `false` to disable the channel.
- `guilds.<id>.channels`: channel rules (keys are channel slugs or ids).
- `guilds.<id>.requireMention`: per-guild mention requirement (overridable per channel).

View File

@@ -89,8 +89,9 @@ function resolveOwnerAllowFromList(params: {
cfg: OpenClawConfig;
accountId?: string | null;
providerId?: ChannelId;
allowFrom?: Array<string | number>;
}): string[] {
const raw = params.cfg.commands?.ownerAllowFrom;
const raw = params.allowFrom ?? params.cfg.commands?.ownerAllowFrom;
if (!Array.isArray(raw) || raw.length === 0) {
return [];
}
@@ -183,11 +184,19 @@ export function resolveCommandAuthorization(params: {
accountId: ctx.AccountId,
allowFrom: Array.isArray(allowFromRaw) ? allowFromRaw : [],
});
const ownerAllowFromList = resolveOwnerAllowFromList({
const configOwnerAllowFromList = resolveOwnerAllowFromList({
dock,
cfg,
accountId: ctx.AccountId,
providerId,
allowFrom: cfg.commands?.ownerAllowFrom,
});
const contextOwnerAllowFromList = resolveOwnerAllowFromList({
dock,
cfg,
accountId: ctx.AccountId,
providerId,
allowFrom: ctx.OwnerAllowFrom,
});
const allowAll =
allowFromList.length === 0 || allowFromList.some((entry) => entry.trim() === "*");
@@ -204,10 +213,19 @@ export function resolveCommandAuthorization(params: {
ownerCandidatesForCommands.push(...normalizedTo);
}
}
const ownerAllowAll = ownerAllowFromList.some((entry) => entry.trim() === "*");
const explicitOwners = ownerAllowFromList.filter((entry) => entry !== "*");
const ownerAllowAll = configOwnerAllowFromList.some((entry) => entry.trim() === "*");
const explicitOwners = configOwnerAllowFromList.filter((entry) => entry !== "*");
const explicitOverrides = contextOwnerAllowFromList.filter((entry) => entry !== "*");
const ownerList = Array.from(
new Set(explicitOwners.length > 0 ? explicitOwners : ownerCandidatesForCommands),
new Set(
explicitOwners.length > 0
? explicitOwners
: ownerAllowAll
? []
: explicitOverrides.length > 0
? explicitOverrides
: ownerCandidatesForCommands,
),
);
const senderCandidates = resolveSenderCandidates({

View File

@@ -167,6 +167,29 @@ describe("resolveCommandAuthorization", () => {
expect(otherAuth.senderIsOwner).toBe(false);
expect(otherAuth.isAuthorizedSender).toBe(false);
});
it("uses owner allowlist override from context when configured", () => {
const cfg = {
channels: { discord: {} },
} as OpenClawConfig;
const ctx = {
Provider: "discord",
Surface: "discord",
From: "discord:123",
SenderId: "123",
OwnerAllowFrom: ["discord:123"],
} as MsgContext;
const auth = resolveCommandAuthorization({
ctx,
cfg,
commandAuthorized: true,
});
expect(auth.senderIsOwner).toBe(true);
expect(auth.ownerList).toEqual(["123"]);
});
});
describe("control command parsing", () => {

View File

@@ -91,6 +91,8 @@ export type MsgContext = {
GroupSystemPrompt?: string;
/** Untrusted metadata that must not be treated as system instructions. */
UntrustedContext?: string[];
/** Explicit owner allowlist overrides (trusted, configuration-derived). */
OwnerAllowFrom?: Array<string | number>;
SenderName?: string;
SenderId?: string;
SenderUsername?: string;

View File

@@ -154,6 +154,30 @@ export function resolveDiscordUserAllowed(params: {
});
}
export function resolveDiscordOwnerAllowFrom(params: {
channelConfig?: DiscordChannelConfigResolved | null;
guildInfo?: DiscordGuildEntryResolved | null;
sender: { id: string; name?: string; tag?: string };
}): string[] | undefined {
const rawAllowList = params.channelConfig?.users ?? params.guildInfo?.users;
if (!Array.isArray(rawAllowList) || rawAllowList.length === 0) {
return undefined;
}
const allowList = normalizeDiscordAllowList(rawAllowList, ["discord:", "user:", "pk:"]);
if (!allowList) {
return undefined;
}
const match = allowListMatches(allowList, {
id: params.sender.id,
name: params.sender.name,
tag: params.sender.tag,
});
if (!match.allowed || !match.matchKey || match.matchKey === "*") {
return undefined;
}
return [match.matchKey];
}
export function resolveDiscordCommandAuthorized(params: {
isDirectMessage: boolean;
allowFrom?: Array<string | number>;

View File

@@ -31,7 +31,7 @@ import { resolveThreadSessionKeys } from "../../routing/session-key.js";
import { buildUntrustedChannelMetadata } from "../../security/channel-metadata.js";
import { truncateUtf16Safe } from "../../utils.js";
import { reactMessageDiscord, removeReactionDiscord } from "../send.js";
import { normalizeDiscordSlug } from "./allow-list.js";
import { normalizeDiscordSlug, resolveDiscordOwnerAllowFrom } from "./allow-list.js";
import { resolveTimestampMs } from "./format.js";
import {
buildDiscordMediaPayload,
@@ -157,6 +157,11 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
);
const groupSystemPrompt =
systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined;
const ownerAllowFrom = resolveDiscordOwnerAllowFrom({
channelConfig,
guildInfo,
sender: { id: sender.id, name: sender.name, tag: sender.tag },
});
const storePath = resolveStorePath(cfg.session?.store, {
agentId: route.agentId,
});
@@ -293,6 +298,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
UntrustedContext: untrustedChannelMetadata ? [untrustedChannelMetadata] : undefined,
GroupSystemPrompt: isGuildMessage ? groupSystemPrompt : undefined,
GroupSpace: isGuildMessage ? (guildInfo?.id ?? guildSlug) || undefined : undefined,
OwnerAllowFrom: ownerAllowFrom,
Provider: "discord" as const,
Surface: "discord" as const,
WasMentioned: effectiveWasMentioned,

View File

@@ -50,6 +50,7 @@ import {
normalizeDiscordSlug,
resolveDiscordChannelConfigWithFallback,
resolveDiscordGuildEntry,
resolveDiscordOwnerAllowFrom,
resolveDiscordUserAllowed,
} from "./allow-list.js";
import { resolveDiscordChannelInfo } from "./message-utils.js";
@@ -741,6 +742,11 @@ async function dispatchDiscordCommandInteraction(params: {
parentPeer: threadParentId ? { kind: "channel", id: threadParentId } : undefined,
});
const conversationLabel = isDirectMessage ? (user.globalName ?? user.username) : channelId;
const ownerAllowFrom = resolveDiscordOwnerAllowFrom({
channelConfig,
guildInfo,
sender: { id: sender.id, name: sender.name, tag: sender.tag },
});
const ctxPayload = finalizeInboundContext({
Body: prompt,
RawBody: prompt,
@@ -778,6 +784,7 @@ async function dispatchDiscordCommandInteraction(params: {
return untrustedChannelMetadata ? [untrustedChannelMetadata] : undefined;
})()
: undefined,
OwnerAllowFrom: ownerAllowFrom,
SenderName: user.globalName ?? user.username,
SenderId: user.id,
SenderUsername: user.username,