mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-18 04:31:10 +00:00
refactor: dedupe config write policy helpers
This commit is contained in:
203
src/channels/plugins/config-write-policy-shared.ts
Normal file
203
src/channels/plugins/config-write-policy-shared.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { resolveAccountEntry } from "../../routing/account-lookup.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
|
||||
|
||||
type AccountConfigWithWrites = {
|
||||
configWrites?: boolean;
|
||||
};
|
||||
|
||||
type ChannelConfigWithAccounts = {
|
||||
configWrites?: boolean;
|
||||
accounts?: Record<string, AccountConfigWithWrites>;
|
||||
};
|
||||
|
||||
export type ConfigWriteScopeLike<TChannelId extends string = string> = {
|
||||
channelId?: TChannelId | null;
|
||||
accountId?: string | null;
|
||||
};
|
||||
|
||||
export type ConfigWriteTargetLike<TChannelId extends string = string> =
|
||||
| { kind: "global" }
|
||||
| { kind: "channel"; scope: { channelId: TChannelId } }
|
||||
| { kind: "account"; scope: { channelId: TChannelId; accountId: string } }
|
||||
| { kind: "ambiguous"; scopes: ConfigWriteScopeLike<TChannelId>[] };
|
||||
|
||||
export type ConfigWriteAuthorizationResultLike<TChannelId extends string = string> =
|
||||
| { allowed: true }
|
||||
| {
|
||||
allowed: false;
|
||||
reason: "ambiguous-target" | "origin-disabled" | "target-disabled";
|
||||
blockedScope?: {
|
||||
kind: "origin" | "target";
|
||||
scope: ConfigWriteScopeLike<TChannelId>;
|
||||
};
|
||||
};
|
||||
|
||||
function listConfigWriteTargetScopes<TChannelId extends string>(
|
||||
target?: ConfigWriteTargetLike<TChannelId>,
|
||||
): ConfigWriteScopeLike<TChannelId>[] {
|
||||
if (!target || target.kind === "global") {
|
||||
return [];
|
||||
}
|
||||
if (target.kind === "ambiguous") {
|
||||
return target.scopes;
|
||||
}
|
||||
return [target.scope];
|
||||
}
|
||||
|
||||
function resolveChannelConfig<TChannelId extends string>(
|
||||
cfg: OpenClawConfig,
|
||||
channelId?: TChannelId | null,
|
||||
): ChannelConfigWithAccounts | undefined {
|
||||
if (!channelId) {
|
||||
return undefined;
|
||||
}
|
||||
return (cfg.channels as Record<string, ChannelConfigWithAccounts> | undefined)?.[channelId];
|
||||
}
|
||||
|
||||
function resolveChannelAccountConfig(
|
||||
channelConfig: ChannelConfigWithAccounts,
|
||||
accountId?: string | null,
|
||||
): AccountConfigWithWrites | undefined {
|
||||
return resolveAccountEntry(channelConfig.accounts, normalizeAccountId(accountId));
|
||||
}
|
||||
|
||||
export function resolveChannelConfigWritesShared<TChannelId extends string>(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channelId?: TChannelId | null;
|
||||
accountId?: string | null;
|
||||
}): boolean {
|
||||
const channelConfig = resolveChannelConfig(params.cfg, params.channelId);
|
||||
if (!channelConfig) {
|
||||
return true;
|
||||
}
|
||||
const accountConfig = resolveChannelAccountConfig(channelConfig, params.accountId);
|
||||
const value = accountConfig?.configWrites ?? channelConfig.configWrites;
|
||||
return value !== false;
|
||||
}
|
||||
|
||||
export function authorizeConfigWriteShared<TChannelId extends string>(params: {
|
||||
cfg: OpenClawConfig;
|
||||
origin?: ConfigWriteScopeLike<TChannelId>;
|
||||
target?: ConfigWriteTargetLike<TChannelId>;
|
||||
allowBypass?: boolean;
|
||||
}): ConfigWriteAuthorizationResultLike<TChannelId> {
|
||||
if (params.allowBypass) {
|
||||
return { allowed: true };
|
||||
}
|
||||
if (params.target?.kind === "ambiguous") {
|
||||
return { allowed: false, reason: "ambiguous-target" };
|
||||
}
|
||||
if (
|
||||
params.origin?.channelId &&
|
||||
!resolveChannelConfigWritesShared({
|
||||
cfg: params.cfg,
|
||||
channelId: params.origin.channelId,
|
||||
accountId: params.origin.accountId,
|
||||
})
|
||||
) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: "origin-disabled",
|
||||
blockedScope: { kind: "origin", scope: params.origin },
|
||||
};
|
||||
}
|
||||
const seen = new Set<string>();
|
||||
for (const target of listConfigWriteTargetScopes(params.target)) {
|
||||
if (!target.channelId) {
|
||||
continue;
|
||||
}
|
||||
const key = `${target.channelId}:${normalizeAccountId(target.accountId)}`;
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
if (
|
||||
!resolveChannelConfigWritesShared({
|
||||
cfg: params.cfg,
|
||||
channelId: target.channelId,
|
||||
accountId: target.accountId,
|
||||
})
|
||||
) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: "target-disabled",
|
||||
blockedScope: { kind: "target", scope: target },
|
||||
};
|
||||
}
|
||||
}
|
||||
return { allowed: true };
|
||||
}
|
||||
|
||||
export function resolveExplicitConfigWriteTargetShared<TChannelId extends string>(
|
||||
scope: ConfigWriteScopeLike<TChannelId>,
|
||||
): ConfigWriteTargetLike<TChannelId> {
|
||||
if (!scope.channelId) {
|
||||
return { kind: "global" };
|
||||
}
|
||||
const accountId = normalizeAccountId(scope.accountId);
|
||||
if (!accountId || accountId === DEFAULT_ACCOUNT_ID) {
|
||||
return { kind: "channel", scope: { channelId: scope.channelId } };
|
||||
}
|
||||
return { kind: "account", scope: { channelId: scope.channelId, accountId } };
|
||||
}
|
||||
|
||||
export function resolveConfigWriteTargetFromPathShared<TChannelId extends string>(params: {
|
||||
path: string[];
|
||||
normalizeChannelId: (raw: string) => TChannelId | null | undefined;
|
||||
}): ConfigWriteTargetLike<TChannelId> {
|
||||
if (params.path[0] !== "channels") {
|
||||
return { kind: "global" };
|
||||
}
|
||||
if (params.path.length < 2) {
|
||||
return { kind: "ambiguous", scopes: [] };
|
||||
}
|
||||
const channelId = params.normalizeChannelId(params.path[1] ?? "");
|
||||
if (!channelId) {
|
||||
return { kind: "ambiguous", scopes: [] };
|
||||
}
|
||||
if (params.path.length === 2) {
|
||||
return { kind: "ambiguous", scopes: [{ channelId }] };
|
||||
}
|
||||
if (params.path[2] !== "accounts") {
|
||||
return { kind: "channel", scope: { channelId } };
|
||||
}
|
||||
if (params.path.length < 4) {
|
||||
return { kind: "ambiguous", scopes: [{ channelId }] };
|
||||
}
|
||||
return resolveExplicitConfigWriteTargetShared({
|
||||
channelId,
|
||||
accountId: normalizeAccountId(params.path[3]),
|
||||
});
|
||||
}
|
||||
|
||||
export function canBypassConfigWritePolicyShared(params: {
|
||||
channel?: string | null;
|
||||
gatewayClientScopes?: string[] | null;
|
||||
isInternalMessageChannel: (channel?: string | null) => boolean;
|
||||
}): boolean {
|
||||
return (
|
||||
params.isInternalMessageChannel(params.channel) &&
|
||||
params.gatewayClientScopes?.includes("operator.admin") === true
|
||||
);
|
||||
}
|
||||
|
||||
export function formatConfigWriteDeniedMessageShared<TChannelId extends string>(params: {
|
||||
result: Exclude<ConfigWriteAuthorizationResultLike<TChannelId>, { allowed: true }>;
|
||||
fallbackChannelId?: TChannelId | null;
|
||||
}): string {
|
||||
if (params.result.reason === "ambiguous-target") {
|
||||
return "⚠️ Channel-initiated /config writes cannot replace channels, channel roots, or accounts collections. Use a more specific path or gateway operator.admin.";
|
||||
}
|
||||
|
||||
const blocked = params.result.blockedScope?.scope;
|
||||
const channelLabel = blocked?.channelId ?? params.fallbackChannelId ?? "this channel";
|
||||
const hint = blocked?.channelId
|
||||
? blocked.accountId
|
||||
? `channels.${blocked.channelId}.accounts.${blocked.accountId}.configWrites=true`
|
||||
: `channels.${blocked.channelId}.configWrites=true`
|
||||
: params.fallbackChannelId
|
||||
? `channels.${params.fallbackChannelId}.configWrites=true`
|
||||
: "channels.<channel>.configWrites=true";
|
||||
return `⚠️ Config writes are disabled for ${channelLabel}. Set ${hint} to enable.`;
|
||||
}
|
||||
@@ -1,49 +1,27 @@
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { resolveAccountEntry } from "../../routing/account-lookup.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
|
||||
import { isInternalMessageChannel } from "../../utils/message-channel.js";
|
||||
import {
|
||||
authorizeConfigWriteShared,
|
||||
canBypassConfigWritePolicyShared,
|
||||
formatConfigWriteDeniedMessageShared,
|
||||
resolveChannelConfigWritesShared,
|
||||
resolveConfigWriteTargetFromPathShared,
|
||||
resolveExplicitConfigWriteTargetShared,
|
||||
type ConfigWriteAuthorizationResultLike,
|
||||
type ConfigWriteScopeLike,
|
||||
type ConfigWriteTargetLike,
|
||||
} from "./config-write-policy-shared.js";
|
||||
import type { ChannelId } from "./types.js";
|
||||
|
||||
type AccountConfigWithWrites = {
|
||||
configWrites?: boolean;
|
||||
};
|
||||
|
||||
type ChannelConfigWithAccounts = {
|
||||
configWrites?: boolean;
|
||||
accounts?: Record<string, AccountConfigWithWrites>;
|
||||
};
|
||||
|
||||
export type ConfigWriteScope = {
|
||||
channelId?: ChannelId | null;
|
||||
accountId?: string | null;
|
||||
};
|
||||
|
||||
export type ConfigWriteTarget =
|
||||
| { kind: "global" }
|
||||
| { kind: "channel"; scope: { channelId: ChannelId } }
|
||||
| { kind: "account"; scope: { channelId: ChannelId; accountId: string } }
|
||||
| { kind: "ambiguous"; scopes: ConfigWriteScope[] };
|
||||
|
||||
export type ConfigWriteAuthorizationResult =
|
||||
| { allowed: true }
|
||||
| {
|
||||
allowed: false;
|
||||
reason: "ambiguous-target" | "origin-disabled" | "target-disabled";
|
||||
blockedScope?: { kind: "origin" | "target"; scope: ConfigWriteScope };
|
||||
};
|
||||
export type ConfigWriteScope = ConfigWriteScopeLike<ChannelId>;
|
||||
export type ConfigWriteTarget = ConfigWriteTargetLike<ChannelId>;
|
||||
export type ConfigWriteAuthorizationResult = ConfigWriteAuthorizationResultLike<ChannelId>;
|
||||
|
||||
export function resolveChannelConfigWrites(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channelId?: ChannelId | null;
|
||||
accountId?: string | null;
|
||||
}): boolean {
|
||||
const channelConfig = resolveChannelConfig(params.cfg, params.channelId);
|
||||
if (!channelConfig) {
|
||||
return true;
|
||||
}
|
||||
const accountConfig = resolveChannelAccountConfig(channelConfig, params.accountId);
|
||||
const value = accountConfig?.configWrites ?? channelConfig.configWrites;
|
||||
return value !== false;
|
||||
return resolveChannelConfigWritesShared(params);
|
||||
}
|
||||
|
||||
export function authorizeConfigWrite(params: {
|
||||
@@ -52,87 +30,17 @@ export function authorizeConfigWrite(params: {
|
||||
target?: ConfigWriteTarget;
|
||||
allowBypass?: boolean;
|
||||
}): ConfigWriteAuthorizationResult {
|
||||
if (params.allowBypass) {
|
||||
return { allowed: true };
|
||||
}
|
||||
if (params.target?.kind === "ambiguous") {
|
||||
return { allowed: false, reason: "ambiguous-target" };
|
||||
}
|
||||
if (
|
||||
params.origin?.channelId &&
|
||||
!resolveChannelConfigWrites({
|
||||
cfg: params.cfg,
|
||||
channelId: params.origin.channelId,
|
||||
accountId: params.origin.accountId,
|
||||
})
|
||||
) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: "origin-disabled",
|
||||
blockedScope: { kind: "origin", scope: params.origin },
|
||||
};
|
||||
}
|
||||
const seen = new Set<string>();
|
||||
for (const target of listConfigWriteTargetScopes(params.target)) {
|
||||
if (!target.channelId) {
|
||||
continue;
|
||||
}
|
||||
const key = `${target.channelId}:${normalizeAccountId(target.accountId)}`;
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
if (
|
||||
!resolveChannelConfigWrites({
|
||||
cfg: params.cfg,
|
||||
channelId: target.channelId,
|
||||
accountId: target.accountId,
|
||||
})
|
||||
) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: "target-disabled",
|
||||
blockedScope: { kind: "target", scope: target },
|
||||
};
|
||||
}
|
||||
}
|
||||
return { allowed: true };
|
||||
return authorizeConfigWriteShared(params);
|
||||
}
|
||||
|
||||
export function resolveExplicitConfigWriteTarget(scope: ConfigWriteScope): ConfigWriteTarget {
|
||||
if (!scope.channelId) {
|
||||
return { kind: "global" };
|
||||
}
|
||||
const accountId = normalizeAccountId(scope.accountId);
|
||||
if (!accountId || accountId === DEFAULT_ACCOUNT_ID) {
|
||||
return { kind: "channel", scope: { channelId: scope.channelId } };
|
||||
}
|
||||
return { kind: "account", scope: { channelId: scope.channelId, accountId } };
|
||||
return resolveExplicitConfigWriteTargetShared(scope);
|
||||
}
|
||||
|
||||
export function resolveConfigWriteTargetFromPath(path: string[]): ConfigWriteTarget {
|
||||
if (path[0] !== "channels") {
|
||||
return { kind: "global" };
|
||||
}
|
||||
if (path.length < 2) {
|
||||
return { kind: "ambiguous", scopes: [] };
|
||||
}
|
||||
const channelId = path[1].trim().toLowerCase() as ChannelId;
|
||||
if (!channelId) {
|
||||
return { kind: "ambiguous", scopes: [] };
|
||||
}
|
||||
if (path.length === 2) {
|
||||
return { kind: "ambiguous", scopes: [{ channelId }] };
|
||||
}
|
||||
if (path[2] !== "accounts") {
|
||||
return { kind: "channel", scope: { channelId } };
|
||||
}
|
||||
if (path.length < 4) {
|
||||
return { kind: "ambiguous", scopes: [{ channelId }] };
|
||||
}
|
||||
return resolveExplicitConfigWriteTarget({
|
||||
channelId,
|
||||
accountId: normalizeAccountId(path[3]),
|
||||
return resolveConfigWriteTargetFromPathShared({
|
||||
path,
|
||||
normalizeChannelId: (raw) => raw.trim().toLowerCase() as ChannelId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -140,55 +48,15 @@ export function canBypassConfigWritePolicy(params: {
|
||||
channel?: string | null;
|
||||
gatewayClientScopes?: string[] | null;
|
||||
}): boolean {
|
||||
return (
|
||||
isInternalMessageChannel(params.channel) &&
|
||||
params.gatewayClientScopes?.includes("operator.admin") === true
|
||||
);
|
||||
return canBypassConfigWritePolicyShared({
|
||||
...params,
|
||||
isInternalMessageChannel,
|
||||
});
|
||||
}
|
||||
|
||||
export function formatConfigWriteDeniedMessage(params: {
|
||||
result: Exclude<ConfigWriteAuthorizationResult, { allowed: true }>;
|
||||
fallbackChannelId?: ChannelId | null;
|
||||
}): string {
|
||||
if (params.result.reason === "ambiguous-target") {
|
||||
return "⚠️ Channel-initiated /config writes cannot replace channels, channel roots, or accounts collections. Use a more specific path or gateway operator.admin.";
|
||||
}
|
||||
|
||||
const blocked = params.result.blockedScope?.scope;
|
||||
const channelLabel = blocked?.channelId ?? params.fallbackChannelId ?? "this channel";
|
||||
const hint = blocked?.channelId
|
||||
? blocked.accountId
|
||||
? `channels.${blocked.channelId}.accounts.${blocked.accountId}.configWrites=true`
|
||||
: `channels.${blocked.channelId}.configWrites=true`
|
||||
: params.fallbackChannelId
|
||||
? `channels.${params.fallbackChannelId}.configWrites=true`
|
||||
: "channels.<channel>.configWrites=true";
|
||||
return `⚠️ Config writes are disabled for ${channelLabel}. Set ${hint} to enable.`;
|
||||
}
|
||||
|
||||
function listConfigWriteTargetScopes(target?: ConfigWriteTarget): ConfigWriteScope[] {
|
||||
if (!target || target.kind === "global") {
|
||||
return [];
|
||||
}
|
||||
if (target.kind === "ambiguous") {
|
||||
return target.scopes;
|
||||
}
|
||||
return [target.scope];
|
||||
}
|
||||
|
||||
function resolveChannelConfig(
|
||||
cfg: OpenClawConfig,
|
||||
channelId?: ChannelId | null,
|
||||
): ChannelConfigWithAccounts | undefined {
|
||||
if (!channelId) {
|
||||
return undefined;
|
||||
}
|
||||
return (cfg.channels as Record<string, ChannelConfigWithAccounts> | undefined)?.[channelId];
|
||||
}
|
||||
|
||||
function resolveChannelAccountConfig(
|
||||
channelConfig: ChannelConfigWithAccounts,
|
||||
accountId?: string | null,
|
||||
): AccountConfigWithWrites | undefined {
|
||||
return resolveAccountEntry(channelConfig.accounts, normalizeAccountId(accountId));
|
||||
return formatConfigWriteDeniedMessageShared(params);
|
||||
}
|
||||
|
||||
@@ -2,42 +2,26 @@ import {
|
||||
deleteAccountFromConfigSection as deleteAccountFromConfigSectionInSection,
|
||||
setAccountEnabledInConfigSection as setAccountEnabledInConfigSectionInSection,
|
||||
} from "../channels/plugins/config-helpers.js";
|
||||
import {
|
||||
authorizeConfigWriteShared,
|
||||
canBypassConfigWritePolicyShared,
|
||||
formatConfigWriteDeniedMessageShared,
|
||||
resolveChannelConfigWritesShared,
|
||||
type ConfigWriteAuthorizationResultLike,
|
||||
type ConfigWriteScopeLike,
|
||||
type ConfigWriteTargetLike,
|
||||
} from "../channels/plugins/config-write-policy-shared.js";
|
||||
import type { ChannelConfigAdapter } from "../channels/plugins/types.adapters.js";
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveAccountEntry } from "../routing/account-lookup.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
|
||||
import { normalizeStringEntries } from "../shared/string-normalization.js";
|
||||
|
||||
const INTERNAL_MESSAGE_CHANNEL = "webchat";
|
||||
|
||||
type AccountConfigWithWrites = {
|
||||
configWrites?: boolean;
|
||||
};
|
||||
|
||||
type ChannelConfigWithAccounts = {
|
||||
configWrites?: boolean;
|
||||
accounts?: Record<string, AccountConfigWithWrites>;
|
||||
};
|
||||
|
||||
export type ConfigWriteScope = {
|
||||
channelId?: string | null;
|
||||
accountId?: string | null;
|
||||
};
|
||||
|
||||
export type ConfigWriteTarget =
|
||||
| { kind: "global" }
|
||||
| { kind: "channel"; scope: { channelId: string } }
|
||||
| { kind: "account"; scope: { channelId: string; accountId: string } }
|
||||
| { kind: "ambiguous"; scopes: ConfigWriteScope[] };
|
||||
|
||||
export type ConfigWriteAuthorizationResult =
|
||||
| { allowed: true }
|
||||
| {
|
||||
allowed: false;
|
||||
reason: "ambiguous-target" | "origin-disabled" | "target-disabled";
|
||||
blockedScope?: { kind: "origin" | "target"; scope: ConfigWriteScope };
|
||||
};
|
||||
export type ConfigWriteScope = ConfigWriteScopeLike;
|
||||
export type ConfigWriteTarget = ConfigWriteTargetLike;
|
||||
export type ConfigWriteAuthorizationResult = ConfigWriteAuthorizationResultLike;
|
||||
|
||||
type ChannelCrudConfigAdapter<ResolvedAccount> = Pick<
|
||||
ChannelConfigAdapter<ResolvedAccount>,
|
||||
@@ -105,45 +89,12 @@ function buildAccountScopedDmSecurityPolicy(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveChannelConfig(
|
||||
cfg: OpenClawConfig,
|
||||
channelId?: string | null,
|
||||
): ChannelConfigWithAccounts | undefined {
|
||||
if (!channelId) {
|
||||
return undefined;
|
||||
}
|
||||
return (cfg.channels as Record<string, ChannelConfigWithAccounts> | undefined)?.[channelId];
|
||||
}
|
||||
|
||||
function resolveChannelAccountConfig(
|
||||
channelConfig: ChannelConfigWithAccounts,
|
||||
accountId?: string | null,
|
||||
): AccountConfigWithWrites | undefined {
|
||||
return resolveAccountEntry(channelConfig.accounts, normalizeAccountId(accountId));
|
||||
}
|
||||
|
||||
function listConfigWriteTargetScopes(target?: ConfigWriteTarget): ConfigWriteScope[] {
|
||||
if (!target || target.kind === "global") {
|
||||
return [];
|
||||
}
|
||||
if (target.kind === "ambiguous") {
|
||||
return target.scopes;
|
||||
}
|
||||
return [target.scope];
|
||||
}
|
||||
|
||||
export function resolveChannelConfigWrites(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channelId?: string | null;
|
||||
accountId?: string | null;
|
||||
}): boolean {
|
||||
const channelConfig = resolveChannelConfig(params.cfg, params.channelId);
|
||||
if (!channelConfig) {
|
||||
return true;
|
||||
}
|
||||
const accountConfig = resolveChannelAccountConfig(channelConfig, params.accountId);
|
||||
const value = accountConfig?.configWrites ?? channelConfig.configWrites;
|
||||
return value !== false;
|
||||
return resolveChannelConfigWritesShared(params);
|
||||
}
|
||||
|
||||
export function authorizeConfigWrite(params: {
|
||||
@@ -152,81 +103,25 @@ export function authorizeConfigWrite(params: {
|
||||
target?: ConfigWriteTarget;
|
||||
allowBypass?: boolean;
|
||||
}): ConfigWriteAuthorizationResult {
|
||||
if (params.allowBypass) {
|
||||
return { allowed: true };
|
||||
}
|
||||
if (params.target?.kind === "ambiguous") {
|
||||
return { allowed: false, reason: "ambiguous-target" };
|
||||
}
|
||||
if (
|
||||
params.origin?.channelId &&
|
||||
!resolveChannelConfigWrites({
|
||||
cfg: params.cfg,
|
||||
channelId: params.origin.channelId,
|
||||
accountId: params.origin.accountId,
|
||||
})
|
||||
) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: "origin-disabled",
|
||||
blockedScope: { kind: "origin", scope: params.origin },
|
||||
};
|
||||
}
|
||||
const seen = new Set<string>();
|
||||
for (const target of listConfigWriteTargetScopes(params.target)) {
|
||||
if (!target.channelId) {
|
||||
continue;
|
||||
}
|
||||
const key = `${target.channelId}:${normalizeAccountId(target.accountId)}`;
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
if (
|
||||
!resolveChannelConfigWrites({
|
||||
cfg: params.cfg,
|
||||
channelId: target.channelId,
|
||||
accountId: target.accountId,
|
||||
})
|
||||
) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: "target-disabled",
|
||||
blockedScope: { kind: "target", scope: target },
|
||||
};
|
||||
}
|
||||
}
|
||||
return { allowed: true };
|
||||
return authorizeConfigWriteShared(params);
|
||||
}
|
||||
|
||||
export function canBypassConfigWritePolicy(params: {
|
||||
channel?: string | null;
|
||||
gatewayClientScopes?: string[] | null;
|
||||
}): boolean {
|
||||
return (
|
||||
params.channel?.trim().toLowerCase() === INTERNAL_MESSAGE_CHANNEL &&
|
||||
params.gatewayClientScopes?.includes("operator.admin") === true
|
||||
);
|
||||
return canBypassConfigWritePolicyShared({
|
||||
...params,
|
||||
isInternalMessageChannel: (channel) =>
|
||||
channel?.trim().toLowerCase() === INTERNAL_MESSAGE_CHANNEL,
|
||||
});
|
||||
}
|
||||
|
||||
export function formatConfigWriteDeniedMessage(params: {
|
||||
result: Exclude<ConfigWriteAuthorizationResult, { allowed: true }>;
|
||||
fallbackChannelId?: string | null;
|
||||
}): string {
|
||||
if (params.result.reason === "ambiguous-target") {
|
||||
return "⚠️ Channel-initiated /config writes cannot replace channels, channel roots, or accounts collections. Use a more specific path or gateway operator.admin.";
|
||||
}
|
||||
|
||||
const blocked = params.result.blockedScope?.scope;
|
||||
const channelLabel = blocked?.channelId ?? params.fallbackChannelId ?? "this channel";
|
||||
const hint = blocked?.channelId
|
||||
? blocked.accountId
|
||||
? `channels.${blocked.channelId}.accounts.${blocked.accountId}.configWrites=true`
|
||||
: `channels.${blocked.channelId}.configWrites=true`
|
||||
: params.fallbackChannelId
|
||||
? `channels.${params.fallbackChannelId}.configWrites=true`
|
||||
: "channels.<channel>.configWrites=true";
|
||||
return `⚠️ Config writes are disabled for ${channelLabel}. Set ${hint} to enable.`;
|
||||
return formatConfigWriteDeniedMessageShared(params);
|
||||
}
|
||||
|
||||
type ChannelConfigAccessorParams<Config extends OpenClawConfig = OpenClawConfig> = {
|
||||
|
||||
Reference in New Issue
Block a user