mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-19 22:10:51 +00:00
184 lines
5.8 KiB
TypeScript
184 lines
5.8 KiB
TypeScript
import type { OpenClawConfig } from "../../config/config.js";
|
|
import { resolveAccountEntry } from "../../routing/account-lookup.js";
|
|
import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js";
|
|
import { normalizeAccountId } from "../../routing/session-key.js";
|
|
import { isInternalMessageChannel } from "../../utils/message-channel.js";
|
|
import type { ChannelId } from "./types.js";
|
|
|
|
type ChannelConfigWithAccounts = {
|
|
configWrites?: boolean;
|
|
accounts?: Record<string, { configWrites?: boolean }>;
|
|
};
|
|
|
|
function resolveAccountConfig(accounts: ChannelConfigWithAccounts["accounts"], accountId: string) {
|
|
return resolveAccountEntry(accounts, accountId);
|
|
}
|
|
|
|
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 function resolveChannelConfigWrites(params: {
|
|
cfg: OpenClawConfig;
|
|
channelId?: ChannelId | null;
|
|
accountId?: string | null;
|
|
}): boolean {
|
|
if (!params.channelId) {
|
|
return true;
|
|
}
|
|
const channels = params.cfg.channels as Record<string, ChannelConfigWithAccounts> | undefined;
|
|
const channelConfig = channels?.[params.channelId];
|
|
if (!channelConfig) {
|
|
return true;
|
|
}
|
|
const accountId = normalizeAccountId(params.accountId);
|
|
const accountConfig = resolveAccountConfig(channelConfig.accounts, accountId);
|
|
const value = accountConfig?.configWrites ?? channelConfig.configWrites;
|
|
return value !== false;
|
|
}
|
|
|
|
export function authorizeConfigWrite(params: {
|
|
cfg: OpenClawConfig;
|
|
origin?: ConfigWriteScope;
|
|
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 };
|
|
}
|
|
|
|
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 } };
|
|
}
|
|
|
|
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]),
|
|
});
|
|
}
|
|
|
|
export function canBypassConfigWritePolicy(params: {
|
|
channel?: string | null;
|
|
gatewayClientScopes?: string[] | null;
|
|
}): boolean {
|
|
return (
|
|
isInternalMessageChannel(params.channel) &&
|
|
params.gatewayClientScopes?.includes("operator.admin") === true
|
|
);
|
|
}
|
|
|
|
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];
|
|
}
|