mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
refactor: centralize channel DM access resolution
This commit is contained in:
@@ -32,6 +32,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/Windows: read listener command lines via PowerShell before falling back to `wmic`, so restart health can recognize OpenClaw listeners on modern Windows installs and avoid long anonymous-port waits. Refs #74280. Thanks @zym951223.
|
||||
- Plugins/runtime-deps: memoize packaged bundled runtime dist-mirror preparation after the first successful pass while keeping source-checkout mirrors refreshable, so constrained Docker/VPS installs avoid repeated root scans before chat turns. Refs #73428, #73421, #73532, and #73477. Thanks @Dimaoggg, @oromeis, @oadiazp, @jmfraga, @bstanbury, @antoniusfelix, and @jkobject.
|
||||
- Channels/Discord: treat bare numeric outbound targets that match the effective Discord DM allowlist as user DMs while preserving account-specific legacy `dm.allowFrom` precedence over inherited root `allowFrom`. (#74303) Thanks @Squirbie.
|
||||
- Channels/Discord/Slack: share one DM policy/allowlist resolver across runtime, setup, allowlist editing, and doctor repair, so legacy `dm.policy` / `dm.allowFrom` compatibility migrates to canonical `dmPolicy` / `allowFrom` without divergent access checks. Thanks @Squirbie.
|
||||
- Control UI: make the chat sidebar split divider focusable, keyboard-resizable, ARIA-described, and pointer-event based so sidebar resizing works without a mouse. Thanks @BunsDev.
|
||||
- Agents/usage: keep PI embedded-run telemetry attributed to the resolved model provider instead of the PI harness label, so OpenRouter and other provider-backed turns report the right provider in session usage and traces. Thanks @vincentkoc.
|
||||
- Agents/attribution: send OpenClaw attribution headers on native OpenAI and Codex traffic, including SDK transports, realtime voice and TTS, device-code auth, WHAM usage, and remote embeddings, so PI-origin defaults no longer leak into provider requests. Thanks @vincentkoc.
|
||||
|
||||
@@ -390,11 +390,11 @@ Example:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="DM policy">
|
||||
`channels.discord.dmPolicy` controls DM access (legacy: `channels.discord.dm.policy`):
|
||||
`channels.discord.dmPolicy` controls DM access. `channels.discord.allowFrom` is the canonical DM allowlist.
|
||||
|
||||
- `pairing` (default)
|
||||
- `allowlist`
|
||||
- `open` (requires `channels.discord.allowFrom` to include `"*"`; legacy: `channels.discord.dm.allowFrom`)
|
||||
- `open` (requires `channels.discord.allowFrom` to include `"*"`)
|
||||
- `disabled`
|
||||
|
||||
If DM policy is not open, unknown users are blocked (or prompted for pairing in `pairing` mode).
|
||||
@@ -406,6 +406,8 @@ Example:
|
||||
- Named accounts inherit `channels.discord.allowFrom` when their own `allowFrom` and legacy `dm.allowFrom` are unset.
|
||||
- Named accounts do not inherit `channels.discord.accounts.default.allowFrom`.
|
||||
|
||||
Legacy `channels.discord.dm.policy` and `channels.discord.dm.allowFrom` still read for compatibility. `openclaw doctor --fix` migrates them to `dmPolicy` and `allowFrom` when it can do so without changing access.
|
||||
|
||||
DM target format for delivery:
|
||||
|
||||
- `user:<id>`
|
||||
|
||||
@@ -464,17 +464,17 @@ Current Slack message actions include `send`, `upload-file`, `download-file`, `r
|
||||
|
||||
<Tabs>
|
||||
<Tab title="DM policy">
|
||||
`channels.slack.dmPolicy` controls DM access (legacy: `channels.slack.dm.policy`):
|
||||
`channels.slack.dmPolicy` controls DM access. `channels.slack.allowFrom` is the canonical DM allowlist.
|
||||
|
||||
- `pairing` (default)
|
||||
- `allowlist`
|
||||
- `open` (requires `channels.slack.allowFrom` to include `"*"`; legacy: `channels.slack.dm.allowFrom`)
|
||||
- `open` (requires `channels.slack.allowFrom` to include `"*"`)
|
||||
- `disabled`
|
||||
|
||||
DM flags:
|
||||
|
||||
- `dm.enabled` (default true)
|
||||
- `channels.slack.allowFrom` (preferred)
|
||||
- `channels.slack.allowFrom`
|
||||
- `dm.allowFrom` (legacy)
|
||||
- `dm.groupEnabled` (group DMs default false)
|
||||
- `dm.groupChannels` (optional MPIM allowlist)
|
||||
@@ -485,6 +485,8 @@ Current Slack message actions include `send`, `upload-file`, `download-file`, `r
|
||||
- Named accounts inherit `channels.slack.allowFrom` when their own `allowFrom` is unset.
|
||||
- Named accounts do not inherit `channels.slack.accounts.default.allowFrom`.
|
||||
|
||||
Legacy `channels.slack.dm.policy` and `channels.slack.dm.allowFrom` still read for compatibility. `openclaw doctor --fix` migrates them to `dmPolicy` and `allowFrom` when it can do so without changing access.
|
||||
|
||||
Pairing in DMs uses `openclaw pairing approve slack <code>`.
|
||||
|
||||
</Tab>
|
||||
|
||||
@@ -472,6 +472,8 @@ should use `resolveInboundMentionDecision({ facts, policy })`.
|
||||
});
|
||||
```
|
||||
|
||||
For channels that accept both canonical top-level DM keys and legacy nested keys, use the helpers from `plugin-sdk/channel-config-helpers`: `resolveChannelDmAccess`, `resolveChannelDmPolicy`, `resolveChannelDmAllowFrom`, and `normalizeChannelDmPolicy` keep account-local values ahead of inherited root values. Pair the same resolver with doctor repair through `normalizeLegacyDmAliases` so runtime and migration read the same contract.
|
||||
|
||||
<Accordion title="What createChatChannelPlugin does for you">
|
||||
Instead of implementing low-level adapter interfaces manually, you pass
|
||||
declarative options and the builder composes them:
|
||||
|
||||
@@ -387,7 +387,7 @@ releases.
|
||||
| `plugin-sdk/channel-setup` | Setup wizard adapters | `createOptionalChannelSetupSurface`, `createOptionalChannelSetupAdapter`, `createOptionalChannelSetupWizard`, plus `DEFAULT_ACCOUNT_ID`, `createTopLevelChannelDmPolicy`, `setSetupChannelEnabled`, `splitSetupEntries` |
|
||||
| `plugin-sdk/channel-pairing` | DM pairing primitives | `createChannelPairingController` |
|
||||
| `plugin-sdk/channel-reply-pipeline` | Reply prefix, typing, and source-delivery wiring | `createChannelReplyPipeline`, `resolveChannelSourceReplyDeliveryMode` |
|
||||
| `plugin-sdk/channel-config-helpers` | Config adapter factories | `createHybridChannelConfigAdapter` |
|
||||
| `plugin-sdk/channel-config-helpers` | Config adapter factories and DM access helpers | `createHybridChannelConfigAdapter`, `resolveChannelDmAccess`, `resolveChannelDmAllowFrom`, `resolveChannelDmPolicy`, `normalizeChannelDmPolicy`, `normalizeLegacyDmAliases` |
|
||||
| `plugin-sdk/channel-config-schema` | Config schema builders | Shared channel config schema primitives and the generic builder only |
|
||||
| `plugin-sdk/bundled-channel-config-schema` | Bundled config schemas | OpenClaw-maintained bundled plugins only; new plugins must define plugin-local schemas |
|
||||
| `plugin-sdk/channel-config-schema-legacy` | Deprecated bundled config schemas | Compatibility alias only; use `plugin-sdk/bundled-channel-config-schema` for maintained bundled plugins |
|
||||
|
||||
@@ -57,7 +57,7 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview)
|
||||
| `plugin-sdk/account-helpers` | Narrow account-list/account-action helpers |
|
||||
| `plugin-sdk/channel-pairing` | `createChannelPairingController` |
|
||||
| `plugin-sdk/channel-reply-pipeline` | `createChannelReplyPipeline`, `resolveChannelSourceReplyDeliveryMode` |
|
||||
| `plugin-sdk/channel-config-helpers` | `createHybridChannelConfigAdapter` |
|
||||
| `plugin-sdk/channel-config-helpers` | `createHybridChannelConfigAdapter`, `resolveChannelDmAccess`, `resolveChannelDmAllowFrom`, `resolveChannelDmPolicy`, `normalizeChannelDmPolicy`, `normalizeLegacyDmAliases` |
|
||||
| `plugin-sdk/channel-config-schema` | Shared channel config schema primitives and generic builder |
|
||||
| `plugin-sdk/bundled-channel-config-schema` | Bundled OpenClaw channel config schemas for maintained bundled plugins only |
|
||||
| `plugin-sdk/channel-config-schema-legacy` | Deprecated compatibility alias for bundled-channel config schemas |
|
||||
|
||||
@@ -4,6 +4,12 @@ import {
|
||||
resolveMergedAccountConfig,
|
||||
} from "openclaw/plugin-sdk/account-helpers";
|
||||
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import {
|
||||
normalizeChannelDmPolicy,
|
||||
resolveChannelDmAllowFrom,
|
||||
resolveChannelDmPolicy,
|
||||
type ChannelDmPolicy,
|
||||
} from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { resolveAccountEntry } from "openclaw/plugin-sdk/routing";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import type { DiscordAccountConfig, DiscordActionConfig, OpenClawConfig } from "./runtime-api.js";
|
||||
@@ -52,14 +58,27 @@ export function resolveDiscordAccountAllowFrom(params: {
|
||||
const accountConfig = resolveDiscordAccountConfig(params.cfg, accountId);
|
||||
const rootConfig = params.cfg.channels?.discord as DiscordAccountConfig | undefined;
|
||||
|
||||
// New allowFrom wins over legacy dm.allowFrom within the same scope, while an
|
||||
// account-local legacy list still wins over a root allowFrom inherited by merge.
|
||||
return (
|
||||
accountConfig?.allowFrom ??
|
||||
accountConfig?.dm?.allowFrom ??
|
||||
rootConfig?.allowFrom ??
|
||||
rootConfig?.dm?.allowFrom
|
||||
return resolveChannelDmAllowFrom({
|
||||
account: accountConfig as Record<string, unknown> | undefined,
|
||||
parent: rootConfig as Record<string, unknown> | undefined,
|
||||
}) as string[] | undefined;
|
||||
}
|
||||
|
||||
export function resolveDiscordAccountDmPolicy(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}): ChannelDmPolicy | undefined {
|
||||
const accountId = normalizeAccountId(
|
||||
params.accountId ?? resolveDefaultDiscordAccountId(params.cfg),
|
||||
);
|
||||
const accountConfig = resolveDiscordAccountConfig(params.cfg, accountId);
|
||||
const rootConfig = params.cfg.channels?.discord as DiscordAccountConfig | undefined;
|
||||
const policy = resolveChannelDmPolicy({
|
||||
account: accountConfig as Record<string, unknown> | undefined,
|
||||
parent: rootConfig as Record<string, unknown> | undefined,
|
||||
defaultPolicy: "pairing",
|
||||
});
|
||||
return normalizeChannelDmPolicy(policy);
|
||||
}
|
||||
|
||||
export function createDiscordActionGate(params: {
|
||||
|
||||
@@ -489,6 +489,8 @@ describe("discordPlugin security", () => {
|
||||
|
||||
expect(result.policy).toBe("allowlist");
|
||||
expect(result.allowFrom).toEqual([" discord:<@!123456789> "]);
|
||||
expect(result.policyPath).toBe("channels.discord.dmPolicy");
|
||||
expect(result.allowFromPath).toBe("channels.discord.");
|
||||
expect(result.normalizeEntry?.(" discord:<@!123456789> ")).toBe("123456789");
|
||||
expect(result.normalizeEntry?.(" user:987654321 ")).toBe("987654321");
|
||||
});
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
import {
|
||||
listDiscordAccountIds,
|
||||
resolveDiscordAccount,
|
||||
resolveDiscordAccountAllowFrom,
|
||||
type ResolvedDiscordAccount,
|
||||
} from "./accounts.js";
|
||||
import { getDiscordApprovalCapability } from "./approval-native.js";
|
||||
@@ -373,7 +374,8 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount, DiscordProbe>
|
||||
resolveAccount: resolveDiscordAccount,
|
||||
normalize: ({ cfg, accountId, values }) =>
|
||||
discordConfigAdapter.formatAllowFrom!({ cfg, accountId, allowFrom: values }),
|
||||
resolveDmAllowFrom: (account) => account.config.allowFrom ?? account.config.dm?.allowFrom,
|
||||
resolveDmAllowFrom: (account, { cfg }) =>
|
||||
resolveDiscordAccountAllowFrom({ cfg, accountId: account.accountId }),
|
||||
resolveGroupPolicy: (account) => account.config.groupPolicy,
|
||||
resolveGroupOverrides: resolveDiscordAllowlistGroupOverrides,
|
||||
}),
|
||||
|
||||
@@ -219,14 +219,12 @@ export function normalizeCompatibilityConfig({
|
||||
const changes: string[] = [];
|
||||
let updated = rawEntry;
|
||||
let changed = false;
|
||||
const shouldPromoteRootDmAllowFrom = !asObjectRecord(updated.accounts);
|
||||
|
||||
const aliases = normalizeLegacyChannelAliases({
|
||||
entry: rawEntry,
|
||||
pathPrefix: "channels.discord",
|
||||
changes,
|
||||
normalizeDm: true,
|
||||
rootDmPromoteAllowFrom: shouldPromoteRootDmAllowFrom,
|
||||
normalizeAccountDm: true,
|
||||
resolveStreamingOptions: (entry) => ({
|
||||
resolvedMode: resolveDiscordPreviewStreamMode(entry),
|
||||
|
||||
@@ -300,7 +300,7 @@ function collectDiscordMutableAllowlistWarnings(cfg: OpenClawConfig): string[] {
|
||||
}
|
||||
|
||||
export const discordDoctor: ChannelDoctorAdapter = {
|
||||
dmAllowFromMode: "topOrNested",
|
||||
dmAllowFromMode: "topOnly",
|
||||
groupModel: "route",
|
||||
groupAllowFromFallbackToAllowFrom: false,
|
||||
warnOnEmptyGroupSenderAllowlist: false,
|
||||
|
||||
@@ -99,6 +99,7 @@ export function createDiscordPreflightArgs(params: {
|
||||
replyToMode: "all",
|
||||
dmEnabled: true,
|
||||
groupDmEnabled: true,
|
||||
dmPolicy: params.discordConfig?.dmPolicy ?? params.discordConfig?.dm?.policy ?? "pairing",
|
||||
ackReactionScope: "direct",
|
||||
groupPolicy: "open",
|
||||
threadBindings: createNoopThreadBindingManager("default"),
|
||||
|
||||
@@ -220,7 +220,7 @@ export async function preflightDiscordMessage(
|
||||
return null;
|
||||
}
|
||||
|
||||
const dmPolicy = params.discordConfig?.dmPolicy ?? params.discordConfig?.dm?.policy ?? "pairing";
|
||||
const dmPolicy = params.dmPolicy;
|
||||
const useAccessGroups = params.cfg.commands?.useAccessGroups !== false;
|
||||
const resolvedAccountId = params.accountId ?? resolveDefaultDiscordAccountId(params.cfg);
|
||||
const allowNameMatching = isDangerousNameMatchingEnabled(params.discordConfig);
|
||||
|
||||
@@ -97,6 +97,7 @@ export type DiscordMessagePreflightParams = DiscordMessagePreflightSharedFields
|
||||
dmEnabled: boolean;
|
||||
groupDmEnabled: boolean;
|
||||
groupDmChannels?: string[];
|
||||
dmPolicy: "open" | "pairing" | "allowlist" | "disabled";
|
||||
allowFrom?: string[];
|
||||
guildEntries?: Record<string, DiscordGuildEntryResolved>;
|
||||
ackReactionScope: DiscordMessagePreflightContext["ackReactionScope"];
|
||||
|
||||
@@ -43,6 +43,7 @@ export function createDiscordHandlerParams(overrides?: {
|
||||
textLimit: 2_000,
|
||||
replyToMode: "off" as const,
|
||||
dmEnabled: true,
|
||||
dmPolicy: "pairing",
|
||||
groupDmEnabled: false,
|
||||
threadBindings: createNoopThreadBindingManager("default"),
|
||||
setStatus: overrides?.setStatus,
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||
import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
|
||||
import { resolveOpenProviderRuntimeGroupPolicy } from "openclaw/plugin-sdk/runtime-group-policy";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { resolveDiscordAccountAllowFrom, resolveDiscordAccountDmPolicy } from "../accounts.js";
|
||||
import type { AutocompleteInteraction } from "../internal/discord.js";
|
||||
import {
|
||||
normalizeDiscordAllowList,
|
||||
@@ -183,8 +184,13 @@ export async function resolveDiscordNativeAutocompleteAuthorized(params: {
|
||||
: [];
|
||||
const allowNameMatching = isDangerousNameMatchingEnabled(discordConfig);
|
||||
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
|
||||
const configuredDmAllowFrom =
|
||||
resolveDiscordAccountAllowFrom({
|
||||
cfg,
|
||||
accountId,
|
||||
}) ?? [];
|
||||
const { ownerAllowList, ownerAllowed: ownerOk } = resolveDiscordOwnerAccess({
|
||||
allowFrom: discordConfig?.allowFrom ?? discordConfig?.dm?.allowFrom ?? [],
|
||||
allowFrom: configuredDmAllowFrom,
|
||||
sender: {
|
||||
id: sender.id,
|
||||
name: sender.name,
|
||||
@@ -249,7 +255,7 @@ export async function resolveDiscordNativeAutocompleteAuthorized(params: {
|
||||
}
|
||||
}
|
||||
const dmEnabled = discordConfig?.dm?.enabled ?? true;
|
||||
const dmPolicy = discordConfig?.dmPolicy ?? discordConfig?.dm?.policy ?? "pairing";
|
||||
const dmPolicy = resolveDiscordAccountDmPolicy({ cfg, accountId }) ?? "pairing";
|
||||
if (isDirectMessage) {
|
||||
if (!dmEnabled || dmPolicy === "disabled") {
|
||||
return false;
|
||||
@@ -257,7 +263,7 @@ export async function resolveDiscordNativeAutocompleteAuthorized(params: {
|
||||
const dmAccess = await resolveDiscordDmCommandAccess({
|
||||
accountId,
|
||||
dmPolicy,
|
||||
configuredAllowFrom: discordConfig?.allowFrom ?? discordConfig?.dm?.allowFrom ?? [],
|
||||
configuredAllowFrom: configuredDmAllowFrom,
|
||||
sender: {
|
||||
id: sender.id,
|
||||
name: sender.name,
|
||||
|
||||
@@ -23,7 +23,11 @@ import { dispatchReplyWithDispatcher } from "openclaw/plugin-sdk/reply-dispatch-
|
||||
import { createSubsystemLogger, logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { resolveOpenProviderRuntimeGroupPolicy } from "openclaw/plugin-sdk/runtime-group-policy";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { resolveDiscordMaxLinesPerMessage } from "../accounts.js";
|
||||
import {
|
||||
resolveDiscordAccountAllowFrom,
|
||||
resolveDiscordAccountDmPolicy,
|
||||
resolveDiscordMaxLinesPerMessage,
|
||||
} from "../accounts.js";
|
||||
import {
|
||||
Button,
|
||||
Command,
|
||||
@@ -312,8 +316,13 @@ async function dispatchDiscordCommandInteraction(params: {
|
||||
? interaction.rawData.member.roles.map((roleId: string) => roleId)
|
||||
: [];
|
||||
const allowNameMatching = isDangerousNameMatchingEnabled(discordConfig);
|
||||
const configuredDmAllowFrom =
|
||||
resolveDiscordAccountAllowFrom({
|
||||
cfg,
|
||||
accountId,
|
||||
}) ?? [];
|
||||
const { ownerAllowList, ownerAllowed: ownerOk } = resolveDiscordOwnerAccess({
|
||||
allowFrom: discordConfig?.allowFrom ?? discordConfig?.dm?.allowFrom ?? [],
|
||||
allowFrom: configuredDmAllowFrom,
|
||||
sender: {
|
||||
id: sender.id,
|
||||
name: sender.name,
|
||||
@@ -414,7 +423,7 @@ async function dispatchDiscordCommandInteraction(params: {
|
||||
}
|
||||
}
|
||||
const dmEnabled = discordConfig?.dm?.enabled ?? true;
|
||||
const dmPolicy = discordConfig?.dmPolicy ?? discordConfig?.dm?.policy ?? "pairing";
|
||||
const dmPolicy = resolveDiscordAccountDmPolicy({ cfg, accountId }) ?? "pairing";
|
||||
let commandAuthorized = true;
|
||||
if (isDirectMessage) {
|
||||
if (!dmEnabled || dmPolicy === "disabled") {
|
||||
@@ -424,7 +433,7 @@ async function dispatchDiscordCommandInteraction(params: {
|
||||
const dmAccess = await resolveDiscordDmCommandAccess({
|
||||
accountId,
|
||||
dmPolicy,
|
||||
configuredAllowFrom: discordConfig?.allowFrom ?? discordConfig?.dm?.allowFrom ?? [],
|
||||
configuredAllowFrom: configuredDmAllowFrom,
|
||||
sender: {
|
||||
id: sender.id,
|
||||
name: sender.name,
|
||||
|
||||
@@ -22,7 +22,11 @@ import {
|
||||
warnMissingProviderGroupPolicyFallbackOnce,
|
||||
} from "openclaw/plugin-sdk/runtime-group-policy";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import { resolveDiscordAccount } from "../accounts.js";
|
||||
import {
|
||||
resolveDiscordAccount,
|
||||
resolveDiscordAccountAllowFrom,
|
||||
resolveDiscordAccountDmPolicy,
|
||||
} from "../accounts.js";
|
||||
import { Client } from "../internal/discord.js";
|
||||
import { GatewayCloseCodes } from "../internal/gateway.js";
|
||||
import { fetchDiscordApplicationId, parseApplicationIdFromToken } from "../probe.js";
|
||||
@@ -201,6 +205,10 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
const discordRestFetch = resolveDiscordRestFetch(rawDiscordCfg.proxy, runtime);
|
||||
const discordProxyFetch = resolveDiscordProxyFetchForAccount(account, cfg, runtime);
|
||||
const dmConfig = rawDiscordCfg.dm;
|
||||
const configuredDmAllowFrom = resolveDiscordAccountAllowFrom({
|
||||
cfg,
|
||||
accountId: account.accountId,
|
||||
});
|
||||
let guildEntries = rawDiscordCfg.guilds;
|
||||
const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
|
||||
const providerConfigPresent = cfg.channels?.discord !== undefined;
|
||||
@@ -218,7 +226,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
blockedLabel: GROUP_POLICY_BLOCKED_LABEL.guild,
|
||||
log: (message) => runtime.log?.(warn(message)),
|
||||
});
|
||||
let allowFrom = discordCfg.allowFrom ?? dmConfig?.allowFrom;
|
||||
let allowFrom = configuredDmAllowFrom ?? [];
|
||||
const mediaMaxBytes =
|
||||
(opts.mediaMaxMb ?? discordCfg.mediaMaxMb ?? DEFAULT_DISCORD_MEDIA_MAX_MB) * 1024 * 1024;
|
||||
const textLimit = resolveTextChunkLimit(cfg, "discord", account.accountId, {
|
||||
@@ -230,7 +238,11 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
);
|
||||
const replyToMode = opts.replyToMode ?? discordCfg.replyToMode ?? "off";
|
||||
const dmEnabled = dmConfig?.enabled ?? true;
|
||||
const dmPolicy = discordCfg.dmPolicy ?? dmConfig?.policy ?? "pairing";
|
||||
const dmPolicy =
|
||||
resolveDiscordAccountDmPolicy({
|
||||
cfg,
|
||||
accountId: account.accountId,
|
||||
}) ?? "pairing";
|
||||
const discordProviderSessionRuntime = await loadDiscordProviderSessionRuntime();
|
||||
const threadBindingIdleTimeoutMs =
|
||||
discordProviderSessionRuntime.resolveThreadBindingIdleTimeoutMs({
|
||||
@@ -277,7 +289,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
runtime,
|
||||
});
|
||||
guildEntries = allowlistResolved.guildEntries;
|
||||
allowFrom = allowlistResolved.allowFrom;
|
||||
allowFrom = allowlistResolved.allowFrom ?? [];
|
||||
|
||||
if ((shouldLogVerboseForTesting ?? shouldLogVerbose)()) {
|
||||
logDiscordResolvedConfig({
|
||||
@@ -535,6 +547,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
textLimit,
|
||||
replyToMode,
|
||||
dmEnabled,
|
||||
dmPolicy,
|
||||
groupDmEnabled,
|
||||
groupDmChannels,
|
||||
allowFrom,
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import { createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { createOpenProviderConfiguredRouteWarningCollector } from "openclaw/plugin-sdk/channel-policy";
|
||||
import type { ResolvedDiscordAccount } from "./accounts.js";
|
||||
import {
|
||||
resolveDiscordAccountAllowFrom,
|
||||
resolveDiscordAccountDmPolicy,
|
||||
type ResolvedDiscordAccount,
|
||||
} from "./accounts.js";
|
||||
import type { ChannelPlugin } from "./channel-api.js";
|
||||
|
||||
const resolveDiscordDmPolicy = createScopedDmSecurityResolver<ResolvedDiscordAccount>({
|
||||
channelKey: "discord",
|
||||
resolvePolicy: (account) => account.config.dm?.policy,
|
||||
resolveAllowFrom: (account) => account.config.dm?.allowFrom,
|
||||
allowFromPathSuffix: "dm.",
|
||||
resolvePolicy: (account) => account.config.dmPolicy,
|
||||
resolveAllowFrom: (account) => account.config.allowFrom,
|
||||
resolveAccess: ({ cfg, account }) => ({
|
||||
dmPolicy: resolveDiscordAccountDmPolicy({ cfg, accountId: account.accountId }),
|
||||
allowFrom: resolveDiscordAccountAllowFrom({ cfg, accountId: account.accountId }),
|
||||
}),
|
||||
policyPathSuffix: "dmPolicy",
|
||||
normalizeEntry: (raw) =>
|
||||
raw
|
||||
.trim()
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type WizardPrompter,
|
||||
} from "openclaw/plugin-sdk/setup-runtime";
|
||||
import { formatDocsLink } from "openclaw/plugin-sdk/setup-tools";
|
||||
import { resolveDiscordAccountAllowFrom } from "./accounts.js";
|
||||
import { resolveDiscordChannelAllowlist } from "./resolve-channels.js";
|
||||
import { resolveDiscordUserAllowlist } from "./resolve-users.js";
|
||||
import {
|
||||
@@ -68,10 +69,8 @@ async function promptDiscordAllowFrom(params: {
|
||||
placeholder: "@alice, 123456789012345678",
|
||||
parseId: parseDiscordAllowFromId,
|
||||
invalidWithoutTokenNote: "Bot token missing; use numeric user ids (or mention form) only.",
|
||||
resolveExisting: (account) => {
|
||||
const config = account.config;
|
||||
return config.allowFrom ?? config.dm?.allowFrom ?? [];
|
||||
},
|
||||
resolveExisting: (account, cfg) =>
|
||||
resolveDiscordAccountAllowFrom({ cfg, accountId: account.accountId }) ?? [],
|
||||
resolveToken: (account) =>
|
||||
resolveDiscordToken(params.cfg, { accountId: account.accountId }).token,
|
||||
resolveEntries: async ({ token, entries }) =>
|
||||
|
||||
@@ -45,7 +45,7 @@ async function loadDiscordDoctorModule(): Promise<DiscordDoctorModule> {
|
||||
}
|
||||
|
||||
const discordDoctor: ChannelDoctorAdapter = {
|
||||
dmAllowFromMode: "topOrNested",
|
||||
dmAllowFromMode: "topOnly",
|
||||
groupModel: "route",
|
||||
groupAllowFromFallbackToAllowFrom: false,
|
||||
warnOnEmptyGroupSenderAllowlist: false,
|
||||
|
||||
@@ -98,9 +98,7 @@ export class DiscordVoiceManager {
|
||||
this.botUserId = params.botUserId;
|
||||
this.voiceEnabled = params.discordConfig.voice?.enabled !== false;
|
||||
this.ownerAllowFrom =
|
||||
resolveDiscordAccountAllowFrom({ cfg: params.cfg, accountId: params.accountId }) ??
|
||||
params.discordConfig.allowFrom ??
|
||||
params.discordConfig.dm?.allowFrom;
|
||||
resolveDiscordAccountAllowFrom({ cfg: params.cfg, accountId: params.accountId }) ?? [];
|
||||
this.speakerContext = new DiscordVoiceSpeakerContextResolver({
|
||||
client: params.client,
|
||||
ownerAllowFrom: this.ownerAllowFrom,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveSlackAccount } from "./accounts.js";
|
||||
import { resolveSlackAccount, resolveSlackAccountAllowFrom } from "./accounts.js";
|
||||
|
||||
describe("resolveSlackAccount allowFrom precedence", () => {
|
||||
it("uses configured defaultAccount when accountId is omitted", () => {
|
||||
@@ -107,6 +107,25 @@ describe("resolveSlackAccount allowFrom precedence", () => {
|
||||
expect(resolved.config.allowFrom).toBeUndefined();
|
||||
expect(resolved.config.dm?.allowFrom).toEqual(["U123"]);
|
||||
});
|
||||
|
||||
it("resolves account legacy dm.allowFrom before inherited root allowFrom", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
slack: {
|
||||
allowFrom: ["root"],
|
||||
accounts: {
|
||||
work: {
|
||||
botToken: "xoxb-work",
|
||||
appToken: "xapp-work",
|
||||
dm: { allowFrom: ["account-legacy"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
|
||||
expect(resolveSlackAccountAllowFrom({ cfg, accountId: "work" })).toEqual(["account-legacy"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveSlackAccount active secret surfaces", () => {
|
||||
|
||||
@@ -5,6 +5,12 @@ import {
|
||||
resolveMergedAccountConfig,
|
||||
type OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/account-resolution";
|
||||
import {
|
||||
normalizeChannelDmPolicy,
|
||||
resolveChannelDmAllowFrom,
|
||||
resolveChannelDmPolicy,
|
||||
type ChannelDmPolicy,
|
||||
} from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import type { SlackAccountSurfaceFields } from "./account-surface-fields.js";
|
||||
import type { SlackAccountConfig } from "./runtime-api.js";
|
||||
@@ -42,6 +48,38 @@ export function mergeSlackAccountConfig(
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveSlackAccountAllowFrom(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}): string[] | undefined {
|
||||
const accountId = normalizeAccountId(
|
||||
params.accountId ?? resolveDefaultSlackAccountId(params.cfg),
|
||||
);
|
||||
const accountConfig = params.cfg.channels?.slack?.accounts?.[accountId];
|
||||
const rootConfig = params.cfg.channels?.slack as SlackAccountConfig | undefined;
|
||||
return resolveChannelDmAllowFrom({
|
||||
account: accountConfig as Record<string, unknown> | undefined,
|
||||
parent: rootConfig as Record<string, unknown> | undefined,
|
||||
}) as string[] | undefined;
|
||||
}
|
||||
|
||||
export function resolveSlackAccountDmPolicy(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}): ChannelDmPolicy | undefined {
|
||||
const accountId = normalizeAccountId(
|
||||
params.accountId ?? resolveDefaultSlackAccountId(params.cfg),
|
||||
);
|
||||
const accountConfig = params.cfg.channels?.slack?.accounts?.[accountId];
|
||||
const rootConfig = params.cfg.channels?.slack as SlackAccountConfig | undefined;
|
||||
const policy = resolveChannelDmPolicy({
|
||||
account: accountConfig as Record<string, unknown> | undefined,
|
||||
parent: rootConfig as Record<string, unknown> | undefined,
|
||||
defaultPolicy: "pairing",
|
||||
});
|
||||
return normalizeChannelDmPolicy(policy);
|
||||
}
|
||||
|
||||
export function resolveSlackAccount(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
resolveApprovalApprovers,
|
||||
} from "openclaw/plugin-sdk/approval-auth-runtime";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||
import { resolveSlackAccount } from "./accounts.js";
|
||||
import { resolveSlackAccount, resolveSlackAccountAllowFrom } from "./accounts.js";
|
||||
import { normalizeSlackApproverId } from "./exec-approvals.js";
|
||||
|
||||
export function getSlackApprovalApprovers(params: {
|
||||
@@ -12,8 +12,7 @@ export function getSlackApprovalApprovers(params: {
|
||||
}): string[] {
|
||||
const account = resolveSlackAccount(params).config;
|
||||
return resolveApprovalApprovers({
|
||||
allowFrom: account.allowFrom,
|
||||
extraAllowFrom: account.dm?.allowFrom,
|
||||
allowFrom: resolveSlackAccountAllowFrom(params),
|
||||
defaultTo: account.defaultTo,
|
||||
normalizeApprover: normalizeSlackApproverId,
|
||||
normalizeDefaultTo: normalizeSlackApproverId,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
listSlackAccountIds,
|
||||
resolveDefaultSlackAccountId,
|
||||
resolveSlackAccount,
|
||||
resolveSlackAccountAllowFrom,
|
||||
} from "./accounts.js";
|
||||
import { type ChannelPlugin } from "./channel-api.js";
|
||||
import { SlackChannelConfigSchema } from "./config-schema.js";
|
||||
@@ -22,15 +23,30 @@ const slackSetupWizard = createSlackSetupWizardProxy(async () => ({
|
||||
slackSetupWizard: (await import("./setup-surface.js")).slackSetupWizard,
|
||||
}));
|
||||
|
||||
const slackSetupConfigAdapter = createScopedChannelConfigAdapter<ResolvedSlackAccount>({
|
||||
type SlackSetupConfigAccessorAccount = {
|
||||
allowFrom: string[] | undefined;
|
||||
defaultTo: string | undefined;
|
||||
};
|
||||
|
||||
const slackSetupConfigAdapter = createScopedChannelConfigAdapter<
|
||||
ResolvedSlackAccount,
|
||||
SlackSetupConfigAccessorAccount
|
||||
>({
|
||||
sectionKey: SLACK_CHANNEL,
|
||||
listAccountIds: listSlackAccountIds,
|
||||
resolveAccount: adaptScopedAccountAccessor(resolveSlackAccount),
|
||||
resolveAccessorAccount: (params) => {
|
||||
const account = resolveSlackAccount(params);
|
||||
return {
|
||||
allowFrom: resolveSlackAccountAllowFrom({ cfg: params.cfg, accountId: account.accountId }),
|
||||
defaultTo: account.config.defaultTo,
|
||||
};
|
||||
},
|
||||
defaultAccountId: resolveDefaultSlackAccountId,
|
||||
clearBaseFields: ["botToken", "appToken", "name"],
|
||||
resolveAllowFrom: (account) => account.dm?.allowFrom,
|
||||
resolveAllowFrom: (account) => account.allowFrom,
|
||||
formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }),
|
||||
resolveDefaultTo: (account) => account.config.defaultTo,
|
||||
resolveDefaultTo: (account) => account.defaultTo,
|
||||
});
|
||||
|
||||
export const slackSetupPlugin: ChannelPlugin<ResolvedSlackAccount> = {
|
||||
|
||||
@@ -372,6 +372,8 @@ describe("slackPlugin security", () => {
|
||||
|
||||
expect(result.policy).toBe("allowlist");
|
||||
expect(result.allowFrom).toEqual([" slack:U123 "]);
|
||||
expect(result.policyPath).toBe("channels.slack.dmPolicy");
|
||||
expect(result.allowFromPath).toBe("channels.slack.");
|
||||
expect(result.normalizeEntry?.(" slack:U123 ")).toBe("U123");
|
||||
expect(result.normalizeEntry?.(" user:U999 ")).toBe("U999");
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@ import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import {
|
||||
resolveDefaultSlackAccountId,
|
||||
resolveSlackAccount,
|
||||
resolveSlackAccountAllowFrom,
|
||||
resolveSlackReplyToMode,
|
||||
type ResolvedSlackAccount,
|
||||
} from "./accounts.js";
|
||||
@@ -371,7 +372,8 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount, SlackProbe> = crea
|
||||
resolveAccount: resolveSlackAccount,
|
||||
normalize: ({ cfg, accountId, values }) =>
|
||||
slackConfigAdapter.formatAllowFrom!({ cfg, accountId, allowFrom: values }),
|
||||
resolveDmAllowFrom: (account) => account.config.allowFrom ?? account.config.dm?.allowFrom,
|
||||
resolveDmAllowFrom: (account, { cfg }) =>
|
||||
resolveSlackAccountAllowFrom({ cfg, accountId: account.accountId }),
|
||||
resolveGroupPolicy: (account) => account.groupPolicy,
|
||||
resolveGroupOverrides: resolveSlackAllowlistGroupOverrides,
|
||||
}),
|
||||
|
||||
@@ -4,7 +4,11 @@ import {
|
||||
type DirectoryConfigParams,
|
||||
} from "openclaw/plugin-sdk/directory-config-runtime";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { mergeSlackAccountConfig, resolveDefaultSlackAccountId } from "./accounts.js";
|
||||
import {
|
||||
mergeSlackAccountConfig,
|
||||
resolveDefaultSlackAccountId,
|
||||
resolveSlackAccountAllowFrom,
|
||||
} from "./accounts.js";
|
||||
import { parseSlackTarget } from "./targets.js";
|
||||
|
||||
function resolveSlackDirectoryConfigAccount(
|
||||
@@ -17,6 +21,7 @@ function resolveSlackDirectoryConfigAccount(
|
||||
accountId: resolvedAccountId,
|
||||
config,
|
||||
dm: config.dm,
|
||||
allowFrom: resolveSlackAccountAllowFrom({ cfg, accountId: resolvedAccountId }) ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,11 +31,10 @@ export const listSlackDirectoryPeersFromConfig = createResolvedDirectoryEntriesL
|
||||
kind: "user",
|
||||
resolveAccount: (cfg, accountId) => resolveSlackDirectoryConfigAccount(cfg, accountId),
|
||||
resolveSources: (account) => {
|
||||
const allowFrom = account.config.allowFrom ?? account.dm?.allowFrom ?? [];
|
||||
const channelUsers = Object.values(account.config.channels ?? {}).flatMap(
|
||||
(channel) => channel.users ?? [],
|
||||
);
|
||||
return [allowFrom, Object.keys(account.config.dms ?? {}), channelUsers];
|
||||
return [account.allowFrom, Object.keys(account.config.dms ?? {}), channelUsers];
|
||||
},
|
||||
normalizeId: (raw) => {
|
||||
const mention = raw.match(/^<@([A-Z0-9]+)>$/i);
|
||||
|
||||
@@ -48,7 +48,7 @@ export const collectSlackMutableAllowlistWarnings =
|
||||
});
|
||||
|
||||
export const slackDoctor: ChannelDoctorAdapter = {
|
||||
dmAllowFromMode: "topOrNested",
|
||||
dmAllowFromMode: "topOnly",
|
||||
groupModel: "route",
|
||||
groupAllowFromFallbackToAllowFrom: false,
|
||||
warnOnEmptyGroupSenderAllowlist: false,
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
|
||||
import { normalizeStringEntries } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { installRequestBodyLimitGuard } from "openclaw/plugin-sdk/webhook-request-guards";
|
||||
import { resolveSlackAccount } from "../accounts.js";
|
||||
import { resolveSlackAccount, resolveSlackAccountAllowFrom } from "../accounts.js";
|
||||
import { resolveSlackWebClientOptions } from "../client-options.js";
|
||||
import { isSlackExecApprovalClientEnabled } from "../exec-approvals.js";
|
||||
import { normalizeSlackWebhookPath, registerSlackHttpHandler } from "../http/index.js";
|
||||
@@ -149,7 +149,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
|
||||
const dmEnabled = dmConfig?.enabled ?? true;
|
||||
const dmPolicy = slackCfg.dmPolicy ?? dmConfig?.policy ?? "pairing";
|
||||
let allowFrom = slackCfg.allowFrom ?? dmConfig?.allowFrom;
|
||||
let allowFrom = resolveSlackAccountAllowFrom({ cfg, accountId: account.accountId });
|
||||
const groupDmEnabled = dmConfig?.groupEnabled ?? false;
|
||||
const groupDmChannels = dmConfig?.groupChannels;
|
||||
let channelsConfig = slackCfg.channels;
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
import { createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { createOpenProviderConfiguredRouteWarningCollector } from "openclaw/plugin-sdk/channel-policy";
|
||||
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
|
||||
import type { ResolvedSlackAccount } from "./accounts.js";
|
||||
import {
|
||||
resolveSlackAccountAllowFrom,
|
||||
resolveSlackAccountDmPolicy,
|
||||
type ResolvedSlackAccount,
|
||||
} from "./accounts.js";
|
||||
import type { ChannelPlugin } from "./channel-api.js";
|
||||
|
||||
const resolveSlackDmPolicy = createScopedDmSecurityResolver<ResolvedSlackAccount>({
|
||||
channelKey: "slack",
|
||||
resolvePolicy: (account) => account.dm?.policy,
|
||||
resolveAllowFrom: (account) => account.dm?.allowFrom,
|
||||
allowFromPathSuffix: "dm.",
|
||||
resolvePolicy: (account) => account.config.dmPolicy,
|
||||
resolveAllowFrom: (account) => account.config.allowFrom,
|
||||
resolveAccess: ({ cfg, account }) => ({
|
||||
dmPolicy: resolveSlackAccountDmPolicy({ cfg, accountId: account.accountId }),
|
||||
allowFrom: resolveSlackAccountAllowFrom({ cfg, accountId: account.accountId }),
|
||||
}),
|
||||
policyPathSuffix: "dmPolicy",
|
||||
normalizeEntry: (raw) =>
|
||||
raw
|
||||
.trim()
|
||||
|
||||
@@ -16,6 +16,7 @@ import { formatDocsLink } from "openclaw/plugin-sdk/setup-tools";
|
||||
import {
|
||||
resolveDefaultSlackAccountId,
|
||||
resolveSlackAccount,
|
||||
resolveSlackAccountAllowFrom,
|
||||
type ResolvedSlackAccount,
|
||||
} from "./accounts.js";
|
||||
import { resolveSlackChannelAllowlist } from "./resolve-channels.js";
|
||||
@@ -70,8 +71,8 @@ async function promptSlackAllowFrom(params: {
|
||||
accountId: params.accountId,
|
||||
defaultAccountId: resolveDefaultSlackAccountId(params.cfg),
|
||||
resolveAccount: adaptScopedAccountAccessor(resolveSlackAccount),
|
||||
resolveExisting: (_account, cfg) =>
|
||||
cfg.channels?.slack?.allowFrom ?? cfg.channels?.slack?.dm?.allowFrom ?? [],
|
||||
resolveExisting: (account, cfg) =>
|
||||
resolveSlackAccountAllowFrom({ cfg, accountId: account.accountId }) ?? [],
|
||||
resolveToken: (account) => account.userToken ?? account.botToken ?? "",
|
||||
noteTitle: "Slack allowlist",
|
||||
noteLines: [
|
||||
|
||||
@@ -9,12 +9,14 @@ import {
|
||||
listSlackAccountIds,
|
||||
resolveDefaultSlackAccountId,
|
||||
resolveSlackAccount,
|
||||
resolveSlackAccountAllowFrom,
|
||||
type ResolvedSlackAccount,
|
||||
} from "./accounts.js";
|
||||
import { getChatChannelMeta, type ChannelPlugin } from "./channel-api.js";
|
||||
import { SlackChannelConfigSchema } from "./config-schema.js";
|
||||
import { slackDoctor } from "./doctor.js";
|
||||
import { isSlackInteractiveRepliesEnabled } from "./interactive-replies.js";
|
||||
import type { OpenClawConfig } from "./runtime-api.js";
|
||||
import { collectRuntimeConfigAssignments, secretTargetRegistryEntries } from "./secret-contract.js";
|
||||
import { slackSecurityAdapter } from "./security.js";
|
||||
import { SLACK_CHANNEL } from "./setup-shared.js";
|
||||
@@ -38,16 +40,36 @@ export function isSlackPluginAccountConfigured(account: ResolvedSlackAccount): b
|
||||
return Boolean(account.appToken?.trim());
|
||||
}
|
||||
|
||||
export const slackConfigAdapter = createScopedChannelConfigAdapter<ResolvedSlackAccount>({
|
||||
type SlackConfigAccessorAccount = {
|
||||
allowFrom: string[] | undefined;
|
||||
defaultTo: string | undefined;
|
||||
};
|
||||
|
||||
function resolveSlackConfigAccessorAccount(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}): SlackConfigAccessorAccount {
|
||||
const account = resolveSlackAccount(params);
|
||||
return {
|
||||
allowFrom: resolveSlackAccountAllowFrom({ cfg: params.cfg, accountId: account.accountId }),
|
||||
defaultTo: account.config.defaultTo,
|
||||
};
|
||||
}
|
||||
|
||||
export const slackConfigAdapter = createScopedChannelConfigAdapter<
|
||||
ResolvedSlackAccount,
|
||||
SlackConfigAccessorAccount
|
||||
>({
|
||||
sectionKey: SLACK_CHANNEL,
|
||||
listAccountIds: listSlackAccountIds,
|
||||
resolveAccount: adaptScopedAccountAccessor(resolveSlackAccount),
|
||||
resolveAccessorAccount: resolveSlackConfigAccessorAccount,
|
||||
inspectAccount: adaptScopedAccountAccessor(inspectSlackAccount),
|
||||
defaultAccountId: resolveDefaultSlackAccountId,
|
||||
clearBaseFields: ["botToken", "appToken", "name"],
|
||||
resolveAllowFrom: (account: ResolvedSlackAccount) => account.dm?.allowFrom,
|
||||
resolveAllowFrom: (account) => account.allowFrom,
|
||||
formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }),
|
||||
resolveDefaultTo: (account: ResolvedSlackAccount) => account.config.defaultTo,
|
||||
resolveDefaultTo: (account) => account.defaultTo,
|
||||
});
|
||||
|
||||
export function createSlackPluginBase(params: {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-ke
|
||||
import { normalizeOptionalString } from "../../shared/string-coerce.js";
|
||||
import { normalizeStringEntries } from "../../shared/string-normalization.js";
|
||||
import type { WizardPrompter } from "../../wizard/prompts.js";
|
||||
import { resolveChannelDmAllowFrom, resolveChannelDmPolicy } from "./dm-access.js";
|
||||
import {
|
||||
moveSingleAccountChannelSectionToDefaultAccount,
|
||||
patchScopedAccountConfig,
|
||||
@@ -565,7 +566,9 @@ export function setCompatChannelDmPolicyWithAllowFrom(params: {
|
||||
allowFrom: undefined,
|
||||
dm: undefined,
|
||||
};
|
||||
const existingAllowFrom = channelConfig.allowFrom ?? channelConfig.dm?.allowFrom;
|
||||
const existingAllowFrom = resolveChannelDmAllowFrom({
|
||||
account: channelConfig as Record<string, unknown>,
|
||||
});
|
||||
const allowFrom =
|
||||
params.dmPolicy === "open" ? addWildcardAllowFrom(existingAllowFrom) : undefined;
|
||||
return patchCompatDmChannelConfig({
|
||||
@@ -651,13 +654,11 @@ export function createCompatChannelDmPolicy(params: {
|
||||
accountId && accountId !== DEFAULT_ACCOUNT_ID
|
||||
? channelConfig.accounts?.[accountId]
|
||||
: undefined;
|
||||
return (
|
||||
accountConfig?.dmPolicy ??
|
||||
accountConfig?.dm?.policy ??
|
||||
channelConfig.dmPolicy ??
|
||||
channelConfig.dm?.policy ??
|
||||
"pairing"
|
||||
);
|
||||
return resolveChannelDmPolicy({
|
||||
account: accountConfig as Record<string, unknown> | undefined,
|
||||
parent: channelConfig as Record<string, unknown>,
|
||||
defaultPolicy: "pairing",
|
||||
}) as DmPolicy;
|
||||
},
|
||||
setPolicy: (cfg, policy, accountId) =>
|
||||
accountId && accountId !== DEFAULT_ACCOUNT_ID
|
||||
@@ -670,44 +671,18 @@ export function createCompatChannelDmPolicy(params: {
|
||||
...(policy === "open"
|
||||
? {
|
||||
allowFrom: addWildcardAllowFrom(
|
||||
(
|
||||
cfg.channels?.[params.channel] as
|
||||
| {
|
||||
accounts?: Record<
|
||||
string,
|
||||
{
|
||||
allowFrom?: Array<string | number>;
|
||||
dm?: { allowFrom?: Array<string | number> };
|
||||
}
|
||||
>;
|
||||
}
|
||||
| undefined
|
||||
)?.accounts?.[accountId]?.allowFrom ??
|
||||
(
|
||||
resolveChannelDmAllowFrom({
|
||||
account: (
|
||||
cfg.channels?.[params.channel] as
|
||||
| {
|
||||
allowFrom?: Array<string | number>;
|
||||
dm?: { allowFrom?: Array<string | number> };
|
||||
accounts?: Record<string, Record<string, unknown>>;
|
||||
}
|
||||
| undefined
|
||||
)?.allowFrom ??
|
||||
(
|
||||
cfg.channels?.[params.channel] as
|
||||
| {
|
||||
accounts?: Record<
|
||||
string,
|
||||
{ dm?: { allowFrom?: Array<string | number> } }
|
||||
>;
|
||||
}
|
||||
| undefined
|
||||
)?.accounts?.[accountId]?.dm?.allowFrom ??
|
||||
(
|
||||
cfg.channels?.[params.channel] as
|
||||
| {
|
||||
dm?: { allowFrom?: Array<string | number> };
|
||||
}
|
||||
| undefined
|
||||
)?.dm?.allowFrom,
|
||||
)?.accounts?.[accountId],
|
||||
parent: cfg.channels?.[params.channel] as
|
||||
| Record<string, unknown>
|
||||
| undefined,
|
||||
}),
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
|
||||
@@ -2525,7 +2525,7 @@ describe("doctor config flow", () => {
|
||||
};
|
||||
};
|
||||
expect(cfg.channels.googlechat.dm.allowFrom).toEqual(["*"]);
|
||||
expect(cfg.channels.googlechat.allowFrom).toEqual(["*"]);
|
||||
expect(cfg.channels.googlechat.allowFrom).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not report repeat talk provider normalization on consecutive repair runs", async () => {
|
||||
|
||||
@@ -1 +1 @@
|
||||
export type AllowFromMode = "topOnly" | "topOrNested" | "nestedOnly";
|
||||
export type { ChannelDmAllowFromMode as AllowFromMode } from "../../../channels/plugins/dm-access.js";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { normalizeChatChannelId } from "../../../channels/ids.js";
|
||||
import { setCanonicalDmAllowFrom } from "../../../channels/plugins/dm-access.js";
|
||||
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
|
||||
import { readChannelAllowFromStore } from "../../../pairing/pairing-store.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../routing/session-key.js";
|
||||
@@ -28,41 +29,14 @@ export async function maybeRepairAllowlistPolicyAllowFrom(cfg: OpenClawConfig):
|
||||
}) => {
|
||||
const count = params.allowFrom.length;
|
||||
const noun = count === 1 ? "entry" : "entries";
|
||||
|
||||
if (params.mode === "nestedOnly") {
|
||||
const dmEntry = params.account.dm;
|
||||
const dm =
|
||||
dmEntry && typeof dmEntry === "object" && !Array.isArray(dmEntry)
|
||||
? (dmEntry as Record<string, unknown>)
|
||||
: {};
|
||||
dm.allowFrom = params.allowFrom;
|
||||
params.account.dm = dm;
|
||||
changes.push(
|
||||
`- ${params.prefix}.dm.allowFrom: restored ${count} sender ${noun} from pairing store (dmPolicy="allowlist").`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (params.mode === "topOrNested") {
|
||||
const dmEntry = params.account.dm;
|
||||
const dm =
|
||||
dmEntry && typeof dmEntry === "object" && !Array.isArray(dmEntry)
|
||||
? (dmEntry as Record<string, unknown>)
|
||||
: undefined;
|
||||
const nestedAllowFrom = dm?.allowFrom as Array<string | number> | undefined;
|
||||
if (dm && !Array.isArray(params.account.allowFrom) && Array.isArray(nestedAllowFrom)) {
|
||||
dm.allowFrom = params.allowFrom;
|
||||
changes.push(
|
||||
`- ${params.prefix}.dm.allowFrom: restored ${count} sender ${noun} from pairing store (dmPolicy="allowlist").`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
params.account.allowFrom = params.allowFrom;
|
||||
changes.push(
|
||||
`- ${params.prefix}.allowFrom: restored ${count} sender ${noun} from pairing store (dmPolicy="allowlist").`,
|
||||
);
|
||||
setCanonicalDmAllowFrom({
|
||||
entry: params.account,
|
||||
mode: params.mode,
|
||||
allowFrom: params.allowFrom,
|
||||
pathPrefix: params.prefix,
|
||||
changes,
|
||||
reason: `restored ${count} sender ${noun} from pairing store (dmPolicy="allowlist").`,
|
||||
});
|
||||
};
|
||||
|
||||
const recoverAllowFromForAccount = async (params: {
|
||||
|
||||
@@ -79,10 +79,11 @@ describe("doctor open-policy allowFrom repair", () => {
|
||||
|
||||
expect(result.changes).toEqual([
|
||||
'- channels.discord.dmPolicy: set to "open" (migrated from channels.discord.dm.policy)',
|
||||
'- channels.discord.dm.allowFrom: added "*" (required by dmPolicy="open")',
|
||||
"- channels.discord.dm.allowFrom: removed after moving allowlist to channels.discord.allowFrom",
|
||||
'- channels.discord.allowFrom: added "*" (required by dmPolicy="open")',
|
||||
]);
|
||||
expect(result.config.channels?.discord?.allowFrom).toBeUndefined();
|
||||
expect(result.config.channels?.discord?.dm?.allowFrom).toEqual(["123", "*"]);
|
||||
expect(result.config.channels?.discord?.allowFrom).toEqual(["123", "*"]);
|
||||
expect(result.config.channels?.discord?.dm).toBeUndefined();
|
||||
});
|
||||
|
||||
it("appends wildcard to existing top-level allowFrom", () => {
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { ensureOpenDmPolicyAllowFromWildcard } from "../../../channels/plugins/dm-access.js";
|
||||
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
|
||||
import { normalizeOptionalString } from "../../../shared/string-coerce.js";
|
||||
import { sanitizeForLog } from "../../../terminal/ansi.js";
|
||||
import { resolveAllowFromMode, type AllowFromMode } from "./allow-from-mode.js";
|
||||
import { asObjectRecord } from "./object.js";
|
||||
|
||||
function hasWildcard(list?: Array<string | number>) {
|
||||
return list?.some((v) => normalizeOptionalString(String(v)) === "*") ?? false;
|
||||
}
|
||||
|
||||
export function collectOpenPolicyAllowFromWarnings(params: {
|
||||
changes: string[];
|
||||
doctorFixCommand: string;
|
||||
@@ -38,90 +34,12 @@ export function maybeRepairOpenPolicyAllowFrom(cfg: OpenClawConfig): {
|
||||
prefix: string,
|
||||
mode: AllowFromMode,
|
||||
) => {
|
||||
const dmEntry = account.dm;
|
||||
const dm =
|
||||
dmEntry && typeof dmEntry === "object" && !Array.isArray(dmEntry)
|
||||
? (dmEntry as Record<string, unknown>)
|
||||
: undefined;
|
||||
const dmPolicy =
|
||||
(account.dmPolicy as string | undefined) ?? (dm?.policy as string | undefined) ?? undefined;
|
||||
const canCanonicalizeTopLevel = mode !== "nestedOnly";
|
||||
const hadNestedOpenPolicy =
|
||||
canCanonicalizeTopLevel && account.dmPolicy === undefined && dm?.policy === "open";
|
||||
|
||||
if (dmPolicy !== "open") {
|
||||
return;
|
||||
}
|
||||
|
||||
const topAllowFrom = account.allowFrom as Array<string | number> | undefined;
|
||||
const nestedAllowFrom = dm?.allowFrom as Array<string | number> | undefined;
|
||||
|
||||
if (hadNestedOpenPolicy) {
|
||||
account.dmPolicy = "open";
|
||||
delete dm.policy;
|
||||
changes.push(`- ${prefix}.dmPolicy: set to "open" (migrated from ${prefix}.dm.policy)`);
|
||||
}
|
||||
|
||||
if (
|
||||
canCanonicalizeTopLevel &&
|
||||
!Array.isArray(topAllowFrom) &&
|
||||
Array.isArray(nestedAllowFrom) &&
|
||||
hasWildcard(nestedAllowFrom)
|
||||
) {
|
||||
account.allowFrom = [...nestedAllowFrom];
|
||||
delete dm?.allowFrom;
|
||||
changes.push(
|
||||
`- ${prefix}.allowFrom: moved ${hasWildcard(nestedAllowFrom) ? "wildcard " : ""}allowlist from ${prefix}.dm.allowFrom`,
|
||||
);
|
||||
}
|
||||
|
||||
if (dm && Object.keys(dm).length === 0) {
|
||||
delete account.dm;
|
||||
}
|
||||
|
||||
if (mode === "nestedOnly") {
|
||||
if (hasWildcard(nestedAllowFrom)) {
|
||||
return;
|
||||
}
|
||||
if (dm && Array.isArray(nestedAllowFrom)) {
|
||||
dm.allowFrom = [...nestedAllowFrom, "*"];
|
||||
changes.push(`- ${prefix}.dm.allowFrom: added "*" (required by dmPolicy="open")`);
|
||||
} else {
|
||||
const nextDm = dm ?? {};
|
||||
nextDm.allowFrom = ["*"];
|
||||
account.dm = nextDm;
|
||||
changes.push(`- ${prefix}.dm.allowFrom: set to ["*"] (required by dmPolicy="open")`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === "topOrNested") {
|
||||
if (hasWildcard(topAllowFrom) || hasWildcard(nestedAllowFrom)) {
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(topAllowFrom)) {
|
||||
account.allowFrom = [...topAllowFrom, "*"];
|
||||
changes.push(`- ${prefix}.allowFrom: added "*" (required by dmPolicy="open")`);
|
||||
} else if (dm && Array.isArray(nestedAllowFrom)) {
|
||||
dm.allowFrom = [...nestedAllowFrom, "*"];
|
||||
changes.push(`- ${prefix}.dm.allowFrom: added "*" (required by dmPolicy="open")`);
|
||||
} else {
|
||||
account.allowFrom = ["*"];
|
||||
changes.push(`- ${prefix}.allowFrom: set to ["*"] (required by dmPolicy="open")`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasWildcard(topAllowFrom)) {
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(topAllowFrom)) {
|
||||
account.allowFrom = [...topAllowFrom, "*"];
|
||||
changes.push(`- ${prefix}.allowFrom: added "*" (required by dmPolicy="open")`);
|
||||
} else {
|
||||
account.allowFrom = ["*"];
|
||||
changes.push(`- ${prefix}.allowFrom: set to ["*"] (required by dmPolicy="open")`);
|
||||
}
|
||||
ensureOpenDmPolicyAllowFromWildcard({
|
||||
entry: account,
|
||||
mode,
|
||||
pathPrefix: prefix,
|
||||
changes,
|
||||
});
|
||||
};
|
||||
|
||||
const nextChannels = next.channels as Record<string, Record<string, unknown>>;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { normalizeStringEntries } from "../shared/string-normalization.js";
|
||||
import {
|
||||
normalizeLegacyDmAliases,
|
||||
type CompatMutationResult,
|
||||
} from "../channels/plugins/dm-access.js";
|
||||
|
||||
export type CompatMutationResult = {
|
||||
entry: Record<string, unknown>;
|
||||
changed: boolean;
|
||||
};
|
||||
export { normalizeLegacyDmAliases };
|
||||
export type { CompatMutationResult };
|
||||
|
||||
export type LegacyStreamingAliasOptions = {
|
||||
resolvedMode: string;
|
||||
@@ -44,96 +45,6 @@ function ensureNestedRecord(owner: Record<string, unknown>, key: string): Record
|
||||
return {};
|
||||
}
|
||||
|
||||
function allowFromListsMatch(left: unknown, right: unknown): boolean {
|
||||
if (!Array.isArray(left) || !Array.isArray(right)) {
|
||||
return false;
|
||||
}
|
||||
const normalizedLeft = normalizeStringEntries(left);
|
||||
const normalizedRight = normalizeStringEntries(right);
|
||||
if (normalizedLeft.length !== normalizedRight.length) {
|
||||
return false;
|
||||
}
|
||||
return normalizedLeft.every((value, index) => value === normalizedRight[index]);
|
||||
}
|
||||
|
||||
export function normalizeLegacyDmAliases(params: {
|
||||
entry: Record<string, unknown>;
|
||||
pathPrefix: string;
|
||||
changes: string[];
|
||||
promoteAllowFrom?: boolean;
|
||||
}): CompatMutationResult {
|
||||
let changed = false;
|
||||
let updated: Record<string, unknown> = params.entry;
|
||||
const rawDm = updated.dm;
|
||||
const dm = asObjectRecord(rawDm) ? (structuredClone(rawDm) as Record<string, unknown>) : null;
|
||||
let dmChanged = false;
|
||||
|
||||
const topDmPolicy = updated.dmPolicy;
|
||||
const legacyDmPolicy = dm?.policy;
|
||||
if (topDmPolicy === undefined && legacyDmPolicy !== undefined) {
|
||||
updated = { ...updated, dmPolicy: legacyDmPolicy };
|
||||
changed = true;
|
||||
if (dm) {
|
||||
delete dm.policy;
|
||||
dmChanged = true;
|
||||
}
|
||||
params.changes.push(`Moved ${params.pathPrefix}.dm.policy → ${params.pathPrefix}.dmPolicy.`);
|
||||
} else if (
|
||||
topDmPolicy !== undefined &&
|
||||
legacyDmPolicy !== undefined &&
|
||||
topDmPolicy === legacyDmPolicy
|
||||
) {
|
||||
if (dm) {
|
||||
delete dm.policy;
|
||||
dmChanged = true;
|
||||
params.changes.push(`Removed ${params.pathPrefix}.dm.policy (dmPolicy already set).`);
|
||||
}
|
||||
}
|
||||
|
||||
if (params.promoteAllowFrom !== false) {
|
||||
const topAllowFrom = updated.allowFrom;
|
||||
const legacyAllowFrom = dm?.allowFrom;
|
||||
if (topAllowFrom === undefined && legacyAllowFrom !== undefined) {
|
||||
updated = { ...updated, allowFrom: legacyAllowFrom };
|
||||
changed = true;
|
||||
if (dm) {
|
||||
delete dm.allowFrom;
|
||||
dmChanged = true;
|
||||
}
|
||||
params.changes.push(
|
||||
`Moved ${params.pathPrefix}.dm.allowFrom → ${params.pathPrefix}.allowFrom.`,
|
||||
);
|
||||
} else if (
|
||||
topAllowFrom !== undefined &&
|
||||
legacyAllowFrom !== undefined &&
|
||||
allowFromListsMatch(topAllowFrom, legacyAllowFrom)
|
||||
) {
|
||||
if (dm) {
|
||||
delete dm.allowFrom;
|
||||
dmChanged = true;
|
||||
params.changes.push(`Removed ${params.pathPrefix}.dm.allowFrom (allowFrom already set).`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dm && asObjectRecord(rawDm) && dmChanged) {
|
||||
const keys = Object.keys(dm);
|
||||
if (keys.length === 0) {
|
||||
if (updated.dm !== undefined) {
|
||||
const { dm: _ignored, ...rest } = updated;
|
||||
updated = rest;
|
||||
changed = true;
|
||||
params.changes.push(`Removed empty ${params.pathPrefix}.dm after migration.`);
|
||||
}
|
||||
} else {
|
||||
updated = { ...updated, dm };
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return { entry: updated, changed };
|
||||
}
|
||||
|
||||
export function normalizeLegacyStreamingAliases(
|
||||
params: {
|
||||
entry: Record<string, unknown>;
|
||||
|
||||
@@ -286,12 +286,12 @@ export type DiscordAccountConfig = {
|
||||
/** Thread session behavior. */
|
||||
thread?: DiscordThreadConfig;
|
||||
/**
|
||||
* Alias for dm.policy (prefer this so it inherits cleanly via base->account shallow merge).
|
||||
* Canonical DM policy key. Doctor migrates legacy channels.discord.dm.policy here.
|
||||
* Legacy key: channels.discord.dm.policy.
|
||||
*/
|
||||
dmPolicy?: DmPolicy;
|
||||
/**
|
||||
* Alias for dm.allowFrom (prefer this so it inherits cleanly via base->account shallow merge).
|
||||
* Canonical DM allowlist. Doctor migrates legacy channels.discord.dm.allowFrom here.
|
||||
* Legacy key: channels.discord.dm.allowFrom.
|
||||
*/
|
||||
allowFrom?: string[];
|
||||
|
||||
@@ -187,12 +187,12 @@ export type SlackAccountConfig = {
|
||||
actions?: SlackActionConfig;
|
||||
slashCommand?: SlackSlashCommandConfig;
|
||||
/**
|
||||
* Alias for dm.policy (prefer this so it inherits cleanly via base->account shallow merge).
|
||||
* Canonical DM policy key. Doctor migrates legacy channels.slack.dm.policy here.
|
||||
* Legacy key: channels.slack.dm.policy.
|
||||
*/
|
||||
dmPolicy?: DmPolicy;
|
||||
/**
|
||||
* Alias for dm.allowFrom (prefer this so it inherits cleanly via base->account shallow merge).
|
||||
* Canonical DM allowlist. Doctor migrates legacy channels.slack.dm.allowFrom here.
|
||||
* Legacy key: channels.slack.dm.allowFrom.
|
||||
*/
|
||||
allowFrom?: Array<string | number>;
|
||||
|
||||
@@ -360,12 +360,13 @@ function buildAccountAllowlistAdapter<ResolvedAccount>(params: {
|
||||
resolvePaths: (scope: "dm" | "group") => AllowlistConfigPaths | null;
|
||||
readConfig: (
|
||||
account: ResolvedAccount,
|
||||
context: { cfg: OpenClawConfig; accountId?: string | null },
|
||||
) => Awaited<ReturnType<NonNullable<ChannelAllowlistAdapter["readConfig"]>>>;
|
||||
}): Pick<ChannelAllowlistAdapter, "supportsScope" | "readConfig" | "applyConfigEdit"> {
|
||||
return {
|
||||
supportsScope: params.supportsScope,
|
||||
readConfig: ({ cfg, accountId }) =>
|
||||
params.readConfig(params.resolveAccount({ cfg, accountId })),
|
||||
params.readConfig(params.resolveAccount({ cfg, accountId }), { cfg, accountId }),
|
||||
applyConfigEdit: buildAccountScopedAllowlistConfigEditor({
|
||||
channelId: params.channelId,
|
||||
normalize: params.normalize,
|
||||
@@ -379,7 +380,10 @@ export function buildDmGroupAccountAllowlistAdapter<ResolvedAccount>(params: {
|
||||
channelId: ChannelId;
|
||||
resolveAccount: AllowlistAccountResolver<ResolvedAccount>;
|
||||
normalize: AllowlistNormalizer;
|
||||
resolveDmAllowFrom: (account: ResolvedAccount) => Array<string | number> | null | undefined;
|
||||
resolveDmAllowFrom: (
|
||||
account: ResolvedAccount,
|
||||
context: { cfg: OpenClawConfig; accountId?: string | null },
|
||||
) => Array<string | number> | null | undefined;
|
||||
resolveGroupAllowFrom: (account: ResolvedAccount) => Array<string | number> | null | undefined;
|
||||
resolveDmPolicy?: (account: ResolvedAccount) => string | null | undefined;
|
||||
resolveGroupPolicy?: (account: ResolvedAccount) => string | null | undefined;
|
||||
@@ -391,8 +395,8 @@ export function buildDmGroupAccountAllowlistAdapter<ResolvedAccount>(params: {
|
||||
normalize: params.normalize,
|
||||
supportsScope: ({ scope }) => scope === "dm" || scope === "group" || scope === "all",
|
||||
resolvePaths: resolveDmGroupAllowlistConfigPaths,
|
||||
readConfig: (account) => ({
|
||||
dmAllowFrom: readConfiguredAllowlistEntries(params.resolveDmAllowFrom(account)),
|
||||
readConfig: (account, context) => ({
|
||||
dmAllowFrom: readConfiguredAllowlistEntries(params.resolveDmAllowFrom(account, context)),
|
||||
groupAllowFrom: readConfiguredAllowlistEntries(params.resolveGroupAllowFrom(account)),
|
||||
dmPolicy: params.resolveDmPolicy?.(account) ?? undefined,
|
||||
groupPolicy: params.resolveGroupPolicy?.(account) ?? undefined,
|
||||
@@ -406,7 +410,10 @@ export function buildLegacyDmAccountAllowlistAdapter<ResolvedAccount>(params: {
|
||||
channelId: ChannelId;
|
||||
resolveAccount: AllowlistAccountResolver<ResolvedAccount>;
|
||||
normalize: AllowlistNormalizer;
|
||||
resolveDmAllowFrom: (account: ResolvedAccount) => Array<string | number> | null | undefined;
|
||||
resolveDmAllowFrom: (
|
||||
account: ResolvedAccount,
|
||||
context: { cfg: OpenClawConfig; accountId?: string | null },
|
||||
) => Array<string | number> | null | undefined;
|
||||
resolveGroupPolicy?: (account: ResolvedAccount) => string | null | undefined;
|
||||
resolveGroupOverrides?: (account: ResolvedAccount) => AllowlistGroupOverride[] | undefined;
|
||||
}): Pick<ChannelAllowlistAdapter, "supportsScope" | "readConfig" | "applyConfigEdit"> {
|
||||
@@ -416,8 +423,8 @@ export function buildLegacyDmAccountAllowlistAdapter<ResolvedAccount>(params: {
|
||||
normalize: params.normalize,
|
||||
supportsScope: ({ scope }) => scope === "dm",
|
||||
resolvePaths: resolveLegacyDmAllowlistConfigPaths,
|
||||
readConfig: (account) => ({
|
||||
dmAllowFrom: readConfiguredAllowlistEntries(params.resolveDmAllowFrom(account)),
|
||||
readConfig: (account, context) => ({
|
||||
dmAllowFrom: readConfiguredAllowlistEntries(params.resolveDmAllowFrom(account, context)),
|
||||
groupPolicy: params.resolveGroupPolicy?.(account) ?? undefined,
|
||||
groupOverrides: params.resolveGroupOverrides?.(account),
|
||||
}),
|
||||
|
||||
@@ -12,7 +12,10 @@ import {
|
||||
createTopLevelChannelConfigAdapter,
|
||||
createTopLevelChannelConfigBase,
|
||||
createHybridChannelConfigBase,
|
||||
ensureOpenDmPolicyAllowFromWildcard,
|
||||
mapAllowFromEntries,
|
||||
normalizeLegacyDmAliases,
|
||||
resolveChannelDmAccess,
|
||||
resolveChannelConfigWrites,
|
||||
resolveOptionalConfigString,
|
||||
} from "./channel-config-helpers.js";
|
||||
@@ -119,6 +122,52 @@ describe("resolveOptionalConfigString", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("channel DM access helpers", () => {
|
||||
it("resolves account legacy allowFrom before inherited root allowFrom", () => {
|
||||
expect(
|
||||
resolveChannelDmAccess({
|
||||
account: { dm: { allowFrom: ["account-legacy"] } },
|
||||
parent: { allowFrom: ["root"] },
|
||||
}),
|
||||
).toEqual({ allowFrom: ["account-legacy"], dmPolicy: undefined });
|
||||
});
|
||||
|
||||
it("keeps nested-only channels on dm.allowFrom", () => {
|
||||
const entry = { dmPolicy: "open", allowFrom: ["matrix:@owner"] };
|
||||
const changes: string[] = [];
|
||||
|
||||
ensureOpenDmPolicyAllowFromWildcard({
|
||||
entry,
|
||||
mode: "nestedOnly",
|
||||
pathPrefix: "channels.matrix",
|
||||
changes,
|
||||
});
|
||||
|
||||
expect(entry).toEqual({ dm: { policy: "open", allowFrom: ["matrix:@owner", "*"] } });
|
||||
expect(changes).toEqual([
|
||||
'- channels.matrix.dm.policy: set to "open" (migrated from channels.matrix.dmPolicy)',
|
||||
"- channels.matrix.allowFrom: removed after moving allowlist to channels.matrix.dm.allowFrom",
|
||||
'- channels.matrix.dm.allowFrom: added "*" (required by dmPolicy="open")',
|
||||
]);
|
||||
});
|
||||
|
||||
it("migrates top-canonical legacy dm aliases", () => {
|
||||
const changes: string[] = [];
|
||||
const result = normalizeLegacyDmAliases({
|
||||
entry: { dm: { policy: "allowlist", allowFrom: ["U1"] } },
|
||||
pathPrefix: "channels.slack",
|
||||
changes,
|
||||
});
|
||||
|
||||
expect(result.entry).toEqual({ dmPolicy: "allowlist", allowFrom: ["U1"] });
|
||||
expect(changes).toEqual([
|
||||
"Moved channels.slack.dm.policy → channels.slack.dmPolicy.",
|
||||
"Moved channels.slack.dm.allowFrom → channels.slack.allowFrom.",
|
||||
"Removed empty channels.slack.dm after migration.",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("config write helpers", () => {
|
||||
it("matches account ids case-insensitively", () => {
|
||||
expect(
|
||||
|
||||
@@ -18,6 +18,20 @@ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.j
|
||||
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
|
||||
import { normalizeStringEntries } from "../shared/string-normalization.js";
|
||||
|
||||
export {
|
||||
ensureOpenDmPolicyAllowFromWildcard,
|
||||
normalizeChannelDmPolicy,
|
||||
normalizeLegacyDmAliases,
|
||||
resolveChannelDmAccess,
|
||||
resolveChannelDmAllowFrom,
|
||||
resolveChannelDmPolicy,
|
||||
setCanonicalDmAllowFrom,
|
||||
type ChannelDmAccess,
|
||||
type ChannelDmAllowFromMode,
|
||||
type ChannelDmPolicy,
|
||||
type DmAccessRecord,
|
||||
} from "../channels/plugins/dm-access.js";
|
||||
|
||||
const INTERNAL_MESSAGE_CHANNEL = "webchat";
|
||||
|
||||
export type ConfigWriteScope = ConfigWriteScopeLike;
|
||||
@@ -594,6 +608,14 @@ export function createScopedDmSecurityResolver<
|
||||
channelKey: string;
|
||||
resolvePolicy: (account: ResolvedAccount) => string | null | undefined;
|
||||
resolveAllowFrom: (account: ResolvedAccount) => Array<string | number> | null | undefined;
|
||||
resolveAccess?: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
account: ResolvedAccount;
|
||||
}) => {
|
||||
dmPolicy?: string | null;
|
||||
allowFrom?: Array<string | number> | null;
|
||||
};
|
||||
resolveFallbackAccountId?: (account: ResolvedAccount) => string | null | undefined;
|
||||
defaultPolicy?: string;
|
||||
allowFromPathSuffix?: string;
|
||||
@@ -611,14 +633,15 @@ export function createScopedDmSecurityResolver<
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
account: ResolvedAccount;
|
||||
}) =>
|
||||
buildAccountScopedDmSecurityPolicy({
|
||||
}) => {
|
||||
const access = params.resolveAccess?.({ cfg, accountId, account });
|
||||
return buildAccountScopedDmSecurityPolicy({
|
||||
cfg,
|
||||
channelKey: params.channelKey,
|
||||
accountId,
|
||||
fallbackAccountId: params.resolveFallbackAccountId?.(account) ?? account.accountId,
|
||||
policy: params.resolvePolicy(account),
|
||||
allowFrom: params.resolveAllowFrom(account) ?? [],
|
||||
policy: access?.dmPolicy ?? params.resolvePolicy(account),
|
||||
allowFrom: access?.allowFrom ?? params.resolveAllowFrom(account) ?? [],
|
||||
defaultPolicy: params.defaultPolicy,
|
||||
allowFromPathSuffix: params.allowFromPathSuffix,
|
||||
policyPathSuffix: params.policyPathSuffix,
|
||||
@@ -627,6 +650,7 @@ export function createScopedDmSecurityResolver<
|
||||
normalizeEntry: params.normalizeEntry,
|
||||
inheritSharedDefaultsFromDefaultAccount: params.inheritSharedDefaultsFromDefaultAccount,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export { buildAccountScopedDmSecurityPolicy };
|
||||
|
||||
Reference in New Issue
Block a user