Files
openclaw/extensions/msteams/src/policy.ts
2026-03-13 01:44:42 +00:00

263 lines
7.8 KiB
TypeScript

import type {
AllowlistMatch,
ChannelGroupContext,
GroupPolicy,
GroupToolPolicyConfig,
MSTeamsChannelConfig,
MSTeamsConfig,
MSTeamsReplyStyle,
MSTeamsTeamConfig,
} from "openclaw/plugin-sdk/msteams";
import {
buildChannelKeyCandidates,
evaluateSenderGroupAccessForPolicy,
normalizeChannelSlug,
resolveAllowlistMatchSimple,
resolveToolsBySender,
resolveChannelEntryMatchWithFallback,
resolveNestedAllowlistDecision,
isDangerousNameMatchingEnabled,
} from "openclaw/plugin-sdk/msteams";
export type MSTeamsResolvedRouteConfig = {
teamConfig?: MSTeamsTeamConfig;
channelConfig?: MSTeamsChannelConfig;
allowlistConfigured: boolean;
allowed: boolean;
teamKey?: string;
channelKey?: string;
channelMatchKey?: string;
channelMatchSource?: "direct" | "wildcard";
};
export function resolveMSTeamsRouteConfig(params: {
cfg?: MSTeamsConfig;
teamId?: string | null | undefined;
teamName?: string | null | undefined;
conversationId?: string | null | undefined;
channelName?: string | null | undefined;
allowNameMatching?: boolean;
}): MSTeamsResolvedRouteConfig {
const teamId = params.teamId?.trim();
const teamName = params.teamName?.trim();
const conversationId = params.conversationId?.trim();
const channelName = params.channelName?.trim();
const teams = params.cfg?.teams ?? {};
const allowlistConfigured = Object.keys(teams).length > 0;
const teamCandidates = buildChannelKeyCandidates(
teamId,
params.allowNameMatching ? teamName : undefined,
params.allowNameMatching && teamName ? normalizeChannelSlug(teamName) : undefined,
);
const teamMatch = resolveChannelEntryMatchWithFallback({
entries: teams,
keys: teamCandidates,
wildcardKey: "*",
normalizeKey: normalizeChannelSlug,
});
const teamConfig = teamMatch.entry;
const channels = teamConfig?.channels ?? {};
const channelAllowlistConfigured = Object.keys(channels).length > 0;
const channelCandidates = buildChannelKeyCandidates(
conversationId,
params.allowNameMatching ? channelName : undefined,
params.allowNameMatching && channelName ? normalizeChannelSlug(channelName) : undefined,
);
const channelMatch = resolveChannelEntryMatchWithFallback({
entries: channels,
keys: channelCandidates,
wildcardKey: "*",
normalizeKey: normalizeChannelSlug,
});
const channelConfig = channelMatch.entry;
const allowed = resolveNestedAllowlistDecision({
outerConfigured: allowlistConfigured,
outerMatched: Boolean(teamConfig),
innerConfigured: channelAllowlistConfigured,
innerMatched: Boolean(channelConfig),
});
return {
teamConfig,
channelConfig,
allowlistConfigured,
allowed,
teamKey: teamMatch.matchKey ?? teamMatch.key,
channelKey: channelMatch.matchKey ?? channelMatch.key,
channelMatchKey: channelMatch.matchKey,
channelMatchSource:
channelMatch.matchSource === "direct" || channelMatch.matchSource === "wildcard"
? channelMatch.matchSource
: undefined,
};
}
export function resolveMSTeamsGroupToolPolicy(
params: ChannelGroupContext,
): GroupToolPolicyConfig | undefined {
const cfg = params.cfg.channels?.msteams;
if (!cfg) {
return undefined;
}
const groupId = params.groupId?.trim();
const groupChannel = params.groupChannel?.trim();
const groupSpace = params.groupSpace?.trim();
const allowNameMatching = isDangerousNameMatchingEnabled(cfg);
const resolved = resolveMSTeamsRouteConfig({
cfg,
teamId: groupSpace,
teamName: groupSpace,
conversationId: groupId,
channelName: groupChannel,
allowNameMatching,
});
if (resolved.channelConfig) {
const senderPolicy = resolveToolsBySender({
toolsBySender: resolved.channelConfig.toolsBySender,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
});
if (senderPolicy) {
return senderPolicy;
}
if (resolved.channelConfig.tools) {
return resolved.channelConfig.tools;
}
const teamSenderPolicy = resolveToolsBySender({
toolsBySender: resolved.teamConfig?.toolsBySender,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
});
if (teamSenderPolicy) {
return teamSenderPolicy;
}
return resolved.teamConfig?.tools;
}
if (resolved.teamConfig) {
const teamSenderPolicy = resolveToolsBySender({
toolsBySender: resolved.teamConfig.toolsBySender,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
});
if (teamSenderPolicy) {
return teamSenderPolicy;
}
if (resolved.teamConfig.tools) {
return resolved.teamConfig.tools;
}
}
if (!groupId) {
return undefined;
}
const channelCandidates = buildChannelKeyCandidates(
groupId,
allowNameMatching ? groupChannel : undefined,
allowNameMatching && groupChannel ? normalizeChannelSlug(groupChannel) : undefined,
);
for (const teamConfig of Object.values(cfg.teams ?? {})) {
const match = resolveChannelEntryMatchWithFallback({
entries: teamConfig?.channels ?? {},
keys: channelCandidates,
wildcardKey: "*",
normalizeKey: normalizeChannelSlug,
});
if (match.entry) {
const senderPolicy = resolveToolsBySender({
toolsBySender: match.entry.toolsBySender,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
});
if (senderPolicy) {
return senderPolicy;
}
if (match.entry.tools) {
return match.entry.tools;
}
const teamSenderPolicy = resolveToolsBySender({
toolsBySender: teamConfig?.toolsBySender,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
});
if (teamSenderPolicy) {
return teamSenderPolicy;
}
return teamConfig?.tools;
}
}
return undefined;
}
export type MSTeamsReplyPolicy = {
requireMention: boolean;
replyStyle: MSTeamsReplyStyle;
};
export type MSTeamsAllowlistMatch = AllowlistMatch<"wildcard" | "id" | "name">;
export function resolveMSTeamsAllowlistMatch(params: {
allowFrom: Array<string | number>;
senderId: string;
senderName?: string | null;
allowNameMatching?: boolean;
}): MSTeamsAllowlistMatch {
return resolveAllowlistMatchSimple(params);
}
export function resolveMSTeamsReplyPolicy(params: {
isDirectMessage: boolean;
globalConfig?: MSTeamsConfig;
teamConfig?: MSTeamsTeamConfig;
channelConfig?: MSTeamsChannelConfig;
}): MSTeamsReplyPolicy {
if (params.isDirectMessage) {
return { requireMention: false, replyStyle: "thread" };
}
const requireMention =
params.channelConfig?.requireMention ??
params.teamConfig?.requireMention ??
params.globalConfig?.requireMention ??
true;
const explicitReplyStyle =
params.channelConfig?.replyStyle ??
params.teamConfig?.replyStyle ??
params.globalConfig?.replyStyle;
const replyStyle: MSTeamsReplyStyle =
explicitReplyStyle ?? (requireMention ? "thread" : "top-level");
return { requireMention, replyStyle };
}
export function isMSTeamsGroupAllowed(params: {
groupPolicy: GroupPolicy;
allowFrom: Array<string | number>;
senderId: string;
senderName?: string | null;
allowNameMatching?: boolean;
}): boolean {
return evaluateSenderGroupAccessForPolicy({
groupPolicy: params.groupPolicy,
groupAllowFrom: params.allowFrom.map((entry) => String(entry)),
senderId: params.senderId,
isSenderAllowed: () => resolveMSTeamsAllowlistMatch(params).allowed,
}).allowed;
}