mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
fix: restore discord owner hint from allowlists
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user