fix: fail closed missing provider group policy across message channels (#23367) (thanks @bmendonca3)

This commit is contained in:
Peter Steinberger
2026-02-22 12:17:44 +01:00
parent 78c3c2a542
commit 777817392d
45 changed files with 420 additions and 75 deletions

View File

@@ -4,6 +4,7 @@ import {
createInboundDebouncer,
resolveInboundDebounceMs,
} from "../../auto-reply/inbound-debounce.js";
import { resolveRuntimeGroupPolicy } from "../../config/runtime-group-policy.js";
import { danger } from "../../globals.js";
import type { DiscordMessageEvent, DiscordMessageHandler } from "./listeners.js";
import { preflightDiscordMessage } from "./message-handler.preflight.js";
@@ -23,7 +24,13 @@ type DiscordMessageHandlerParams = Omit<
export function createDiscordMessageHandler(
params: DiscordMessageHandlerParams,
): DiscordMessageHandler {
const groupPolicy = params.discordConfig?.groupPolicy ?? "open";
const { groupPolicy } = resolveRuntimeGroupPolicy({
providerConfigPresent: params.cfg.channels?.discord !== undefined,
groupPolicy: params.discordConfig?.groupPolicy,
defaultGroupPolicy: params.cfg.channels?.defaults?.groupPolicy,
configuredFallbackPolicy: "open",
missingProviderFallbackPolicy: "allowlist",
});
const ackReactionScope = params.cfg.messages?.ackReactionScope ?? "group-mentions";
const debounceMs = resolveInboundDebounceMs({ cfg: params.cfg, channel: "discord" });

View File

@@ -39,6 +39,7 @@ import type { ReplyPayload } from "../../auto-reply/types.js";
import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js";
import { createReplyPrefixOptions } from "../../channels/reply-prefix.js";
import type { OpenClawConfig, loadConfig } from "../../config/config.js";
import { resolveRuntimeGroupPolicy } from "../../config/runtime-group-policy.js";
import { loadSessionStore, resolveStorePath } from "../../config/sessions.js";
import { logVerbose } from "../../globals.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
@@ -1329,8 +1330,15 @@ async function dispatchDiscordCommandInteraction(params: {
const channelAllowlistConfigured =
Boolean(guildInfo?.channels) && Object.keys(guildInfo?.channels ?? {}).length > 0;
const channelAllowed = channelConfig?.allowed !== false;
const { groupPolicy } = resolveRuntimeGroupPolicy({
providerConfigPresent: cfg.channels?.discord !== undefined,
groupPolicy: discordConfig?.groupPolicy,
defaultGroupPolicy: cfg.channels?.defaults?.groupPolicy,
configuredFallbackPolicy: "open",
missingProviderFallbackPolicy: "allowlist",
});
const allowByPolicy = isDiscordGroupAllowedByPolicy({
groupPolicy: discordConfig?.groupPolicy ?? "open",
groupPolicy,
guildAllowlisted: Boolean(guildInfo),
channelAllowlistConfigured,
channelAllowed,

View File

@@ -26,4 +26,13 @@ describe("resolveDiscordRuntimeGroupPolicy", () => {
expect(resolved.groupPolicy).toBe("disabled");
expect(resolved.providerMissingFallbackApplied).toBe(false);
});
it("ignores explicit global defaults when provider config is missing", () => {
const resolved = __testing.resolveDiscordRuntimeGroupPolicy({
providerConfigPresent: false,
defaultGroupPolicy: "open",
});
expect(resolved.groupPolicy).toBe("allowlist");
expect(resolved.providerMissingFallbackApplied).toBe(true);
});
});

View File

@@ -21,6 +21,7 @@ import {
} from "../../config/commands.js";
import type { OpenClawConfig, ReplyToMode } from "../../config/config.js";
import { loadConfig } from "../../config/config.js";
import { resolveRuntimeGroupPolicy } from "../../config/runtime-group-policy.js";
import type { GroupPolicy } from "../../config/types.base.js";
import { danger, logVerbose, shouldLogVerbose, warn } from "../../globals.js";
import { formatErrorMessage } from "../../infra/errors.js";
@@ -179,15 +180,13 @@ function resolveDiscordRuntimeGroupPolicy(params: {
groupPolicy: GroupPolicy;
providerMissingFallbackApplied: boolean;
} {
const groupPolicy =
params.groupPolicy ??
params.defaultGroupPolicy ??
(params.providerConfigPresent ? "open" : "allowlist");
const providerMissingFallbackApplied =
!params.providerConfigPresent &&
params.groupPolicy === undefined &&
params.defaultGroupPolicy === undefined;
return { groupPolicy, providerMissingFallbackApplied };
return resolveRuntimeGroupPolicy({
providerConfigPresent: params.providerConfigPresent,
groupPolicy: params.groupPolicy,
defaultGroupPolicy: params.defaultGroupPolicy,
configuredFallbackPolicy: "open",
missingProviderFallbackPolicy: "allowlist",
});
}
async function deployDiscordCommands(params: {
@@ -265,20 +264,22 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
const runtime: RuntimeEnv = opts.runtime ?? createNonExitingRuntime();
const discordCfg = account.config;
const rawDiscordCfg = account.config;
const discordRootThreadBindings = cfg.channels?.discord?.threadBindings;
const discordAccountThreadBindings =
cfg.channels?.discord?.accounts?.[account.accountId]?.threadBindings;
const discordRestFetch = resolveDiscordRestFetch(discordCfg.proxy, runtime);
const dmConfig = discordCfg.dm;
let guildEntries = discordCfg.guilds;
const discordRestFetch = resolveDiscordRestFetch(rawDiscordCfg.proxy, runtime);
const dmConfig = rawDiscordCfg.dm;
let guildEntries = rawDiscordCfg.guilds;
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
const providerConfigPresent = cfg.channels?.discord !== undefined;
const { groupPolicy, providerMissingFallbackApplied } = resolveDiscordRuntimeGroupPolicy({
providerConfigPresent,
groupPolicy: discordCfg.groupPolicy,
groupPolicy: rawDiscordCfg.groupPolicy,
defaultGroupPolicy,
});
const discordCfg =
rawDiscordCfg.groupPolicy === groupPolicy ? rawDiscordCfg : { ...rawDiscordCfg, groupPolicy };
if (providerMissingFallbackApplied) {
runtime.log?.(
warn(