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.
|
- Web UI: apply button styling to the new-messages indicator.
|
||||||
- Onboarding: infer auth choice from non-interactive API key flags. (#8484) Thanks @f-trycua.
|
- 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.
|
- 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: 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: 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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).
|
- 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`.
|
- 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>.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>.users`: optional per-channel user allowlist.
|
||||||
- `guilds.<id>.channels.<channel>.skills`: skill filter (omit = all skills, empty = none).
|
- `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>.enabled`: set `false` to disable the channel.
|
||||||
- `guilds.<id>.channels`: channel rules (keys are channel slugs or ids).
|
- `guilds.<id>.channels`: channel rules (keys are channel slugs or ids).
|
||||||
- `guilds.<id>.requireMention`: per-guild mention requirement (overridable per channel).
|
- `guilds.<id>.requireMention`: per-guild mention requirement (overridable per channel).
|
||||||
|
|||||||
@@ -89,8 +89,9 @@ function resolveOwnerAllowFromList(params: {
|
|||||||
cfg: OpenClawConfig;
|
cfg: OpenClawConfig;
|
||||||
accountId?: string | null;
|
accountId?: string | null;
|
||||||
providerId?: ChannelId;
|
providerId?: ChannelId;
|
||||||
|
allowFrom?: Array<string | number>;
|
||||||
}): string[] {
|
}): string[] {
|
||||||
const raw = params.cfg.commands?.ownerAllowFrom;
|
const raw = params.allowFrom ?? params.cfg.commands?.ownerAllowFrom;
|
||||||
if (!Array.isArray(raw) || raw.length === 0) {
|
if (!Array.isArray(raw) || raw.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -183,11 +184,19 @@ export function resolveCommandAuthorization(params: {
|
|||||||
accountId: ctx.AccountId,
|
accountId: ctx.AccountId,
|
||||||
allowFrom: Array.isArray(allowFromRaw) ? allowFromRaw : [],
|
allowFrom: Array.isArray(allowFromRaw) ? allowFromRaw : [],
|
||||||
});
|
});
|
||||||
const ownerAllowFromList = resolveOwnerAllowFromList({
|
const configOwnerAllowFromList = resolveOwnerAllowFromList({
|
||||||
dock,
|
dock,
|
||||||
cfg,
|
cfg,
|
||||||
accountId: ctx.AccountId,
|
accountId: ctx.AccountId,
|
||||||
providerId,
|
providerId,
|
||||||
|
allowFrom: cfg.commands?.ownerAllowFrom,
|
||||||
|
});
|
||||||
|
const contextOwnerAllowFromList = resolveOwnerAllowFromList({
|
||||||
|
dock,
|
||||||
|
cfg,
|
||||||
|
accountId: ctx.AccountId,
|
||||||
|
providerId,
|
||||||
|
allowFrom: ctx.OwnerAllowFrom,
|
||||||
});
|
});
|
||||||
const allowAll =
|
const allowAll =
|
||||||
allowFromList.length === 0 || allowFromList.some((entry) => entry.trim() === "*");
|
allowFromList.length === 0 || allowFromList.some((entry) => entry.trim() === "*");
|
||||||
@@ -204,10 +213,19 @@ export function resolveCommandAuthorization(params: {
|
|||||||
ownerCandidatesForCommands.push(...normalizedTo);
|
ownerCandidatesForCommands.push(...normalizedTo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const ownerAllowAll = ownerAllowFromList.some((entry) => entry.trim() === "*");
|
const ownerAllowAll = configOwnerAllowFromList.some((entry) => entry.trim() === "*");
|
||||||
const explicitOwners = ownerAllowFromList.filter((entry) => entry !== "*");
|
const explicitOwners = configOwnerAllowFromList.filter((entry) => entry !== "*");
|
||||||
|
const explicitOverrides = contextOwnerAllowFromList.filter((entry) => entry !== "*");
|
||||||
const ownerList = Array.from(
|
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({
|
const senderCandidates = resolveSenderCandidates({
|
||||||
|
|||||||
@@ -167,6 +167,29 @@ describe("resolveCommandAuthorization", () => {
|
|||||||
expect(otherAuth.senderIsOwner).toBe(false);
|
expect(otherAuth.senderIsOwner).toBe(false);
|
||||||
expect(otherAuth.isAuthorizedSender).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", () => {
|
describe("control command parsing", () => {
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ export type MsgContext = {
|
|||||||
GroupSystemPrompt?: string;
|
GroupSystemPrompt?: string;
|
||||||
/** Untrusted metadata that must not be treated as system instructions. */
|
/** Untrusted metadata that must not be treated as system instructions. */
|
||||||
UntrustedContext?: string[];
|
UntrustedContext?: string[];
|
||||||
|
/** Explicit owner allowlist overrides (trusted, configuration-derived). */
|
||||||
|
OwnerAllowFrom?: Array<string | number>;
|
||||||
SenderName?: string;
|
SenderName?: string;
|
||||||
SenderId?: string;
|
SenderId?: string;
|
||||||
SenderUsername?: 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: {
|
export function resolveDiscordCommandAuthorized(params: {
|
||||||
isDirectMessage: boolean;
|
isDirectMessage: boolean;
|
||||||
allowFrom?: Array<string | number>;
|
allowFrom?: Array<string | number>;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import { resolveThreadSessionKeys } from "../../routing/session-key.js";
|
|||||||
import { buildUntrustedChannelMetadata } from "../../security/channel-metadata.js";
|
import { buildUntrustedChannelMetadata } from "../../security/channel-metadata.js";
|
||||||
import { truncateUtf16Safe } from "../../utils.js";
|
import { truncateUtf16Safe } from "../../utils.js";
|
||||||
import { reactMessageDiscord, removeReactionDiscord } from "../send.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 { resolveTimestampMs } from "./format.js";
|
||||||
import {
|
import {
|
||||||
buildDiscordMediaPayload,
|
buildDiscordMediaPayload,
|
||||||
@@ -157,6 +157,11 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
|||||||
);
|
);
|
||||||
const groupSystemPrompt =
|
const groupSystemPrompt =
|
||||||
systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined;
|
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, {
|
const storePath = resolveStorePath(cfg.session?.store, {
|
||||||
agentId: route.agentId,
|
agentId: route.agentId,
|
||||||
});
|
});
|
||||||
@@ -293,6 +298,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
|||||||
UntrustedContext: untrustedChannelMetadata ? [untrustedChannelMetadata] : undefined,
|
UntrustedContext: untrustedChannelMetadata ? [untrustedChannelMetadata] : undefined,
|
||||||
GroupSystemPrompt: isGuildMessage ? groupSystemPrompt : undefined,
|
GroupSystemPrompt: isGuildMessage ? groupSystemPrompt : undefined,
|
||||||
GroupSpace: isGuildMessage ? (guildInfo?.id ?? guildSlug) || undefined : undefined,
|
GroupSpace: isGuildMessage ? (guildInfo?.id ?? guildSlug) || undefined : undefined,
|
||||||
|
OwnerAllowFrom: ownerAllowFrom,
|
||||||
Provider: "discord" as const,
|
Provider: "discord" as const,
|
||||||
Surface: "discord" as const,
|
Surface: "discord" as const,
|
||||||
WasMentioned: effectiveWasMentioned,
|
WasMentioned: effectiveWasMentioned,
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import {
|
|||||||
normalizeDiscordSlug,
|
normalizeDiscordSlug,
|
||||||
resolveDiscordChannelConfigWithFallback,
|
resolveDiscordChannelConfigWithFallback,
|
||||||
resolveDiscordGuildEntry,
|
resolveDiscordGuildEntry,
|
||||||
|
resolveDiscordOwnerAllowFrom,
|
||||||
resolveDiscordUserAllowed,
|
resolveDiscordUserAllowed,
|
||||||
} from "./allow-list.js";
|
} from "./allow-list.js";
|
||||||
import { resolveDiscordChannelInfo } from "./message-utils.js";
|
import { resolveDiscordChannelInfo } from "./message-utils.js";
|
||||||
@@ -741,6 +742,11 @@ async function dispatchDiscordCommandInteraction(params: {
|
|||||||
parentPeer: threadParentId ? { kind: "channel", id: threadParentId } : undefined,
|
parentPeer: threadParentId ? { kind: "channel", id: threadParentId } : undefined,
|
||||||
});
|
});
|
||||||
const conversationLabel = isDirectMessage ? (user.globalName ?? user.username) : channelId;
|
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({
|
const ctxPayload = finalizeInboundContext({
|
||||||
Body: prompt,
|
Body: prompt,
|
||||||
RawBody: prompt,
|
RawBody: prompt,
|
||||||
@@ -778,6 +784,7 @@ async function dispatchDiscordCommandInteraction(params: {
|
|||||||
return untrustedChannelMetadata ? [untrustedChannelMetadata] : undefined;
|
return untrustedChannelMetadata ? [untrustedChannelMetadata] : undefined;
|
||||||
})()
|
})()
|
||||||
: undefined,
|
: undefined,
|
||||||
|
OwnerAllowFrom: ownerAllowFrom,
|
||||||
SenderName: user.globalName ?? user.username,
|
SenderName: user.globalName ?? user.username,
|
||||||
SenderId: user.id,
|
SenderId: user.id,
|
||||||
SenderUsername: user.username,
|
SenderUsername: user.username,
|
||||||
|
|||||||
Reference in New Issue
Block a user