openclaw/src/sessions/send-policy.ts

124 lines
3.6 KiB
TypeScript

import { normalizeChatType } from "../channels/chat-type.js";
import type { OpenClawConfig } from "../config/config.js";
import type { SessionChatType, SessionEntry } from "../config/sessions.js";
import { deriveSessionChatType } from "./session-key-utils.js";
export type SessionSendPolicyDecision = "allow" | "deny";
export function normalizeSendPolicy(raw?: string | null): SessionSendPolicyDecision | undefined {
const value = raw?.trim().toLowerCase();
if (value === "allow") {
return "allow";
}
if (value === "deny") {
return "deny";
}
return undefined;
}
function normalizeMatchValue(raw?: string | null) {
const value = raw?.trim().toLowerCase();
return value ? value : undefined;
}
function stripAgentSessionKeyPrefix(key?: string): string | undefined {
if (!key) {
return undefined;
}
const parts = key.split(":").filter(Boolean);
// Canonical agent session keys: agent:<agentId>:<sessionKey...>
if (parts.length >= 3 && parts[0] === "agent") {
return parts.slice(2).join(":");
}
return key;
}
function deriveChannelFromKey(key?: string) {
const normalizedKey = stripAgentSessionKeyPrefix(key);
if (!normalizedKey) {
return undefined;
}
const parts = normalizedKey.split(":").filter(Boolean);
if (parts.length >= 3 && (parts[1] === "group" || parts[1] === "channel")) {
return normalizeMatchValue(parts[0]);
}
return undefined;
}
function deriveChatTypeFromKey(key?: string): SessionChatType | undefined {
const chatType = deriveSessionChatType(key);
return chatType === "unknown" ? undefined : chatType;
}
export function resolveSendPolicy(params: {
cfg: OpenClawConfig;
entry?: SessionEntry;
sessionKey?: string;
channel?: string;
chatType?: SessionChatType;
}): SessionSendPolicyDecision {
const override = normalizeSendPolicy(params.entry?.sendPolicy);
if (override) {
return override;
}
const policy = params.cfg.session?.sendPolicy;
if (!policy) {
return "allow";
}
const channel =
normalizeMatchValue(params.channel) ??
normalizeMatchValue(params.entry?.channel) ??
normalizeMatchValue(params.entry?.lastChannel) ??
deriveChannelFromKey(params.sessionKey);
const chatType =
normalizeChatType(params.chatType ?? params.entry?.chatType) ??
normalizeChatType(deriveChatTypeFromKey(params.sessionKey));
const rawSessionKey = params.sessionKey ?? "";
const strippedSessionKey = stripAgentSessionKeyPrefix(rawSessionKey) ?? "";
const rawSessionKeyNorm = rawSessionKey.toLowerCase();
const strippedSessionKeyNorm = strippedSessionKey.toLowerCase();
let allowedMatch = false;
for (const rule of policy.rules ?? []) {
if (!rule) {
continue;
}
const action = normalizeSendPolicy(rule.action) ?? "allow";
const match = rule.match ?? {};
const matchChannel = normalizeMatchValue(match.channel);
const matchChatType = normalizeChatType(match.chatType);
const matchPrefix = normalizeMatchValue(match.keyPrefix);
const matchRawPrefix = normalizeMatchValue(match.rawKeyPrefix);
if (matchChannel && matchChannel !== channel) {
continue;
}
if (matchChatType && matchChatType !== chatType) {
continue;
}
if (matchRawPrefix && !rawSessionKeyNorm.startsWith(matchRawPrefix)) {
continue;
}
if (
matchPrefix &&
!rawSessionKeyNorm.startsWith(matchPrefix) &&
!strippedSessionKeyNorm.startsWith(matchPrefix)
) {
continue;
}
if (action === "deny") {
return "deny";
}
allowedMatch = true;
}
if (allowedMatch) {
return "allow";
}
const fallback = normalizeSendPolicy(policy.default);
return fallback ?? "allow";
}