mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-20 21:23:23 +00:00
refactor(approvals): share request filter matching
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
type TopLevelComponents,
|
||||
} from "@buape/carbon";
|
||||
import { ButtonStyle, Routes } from "discord-api-types/v10";
|
||||
import { matchesApprovalRequestFilters } from "openclaw/plugin-sdk/approval-runtime";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import type { DiscordExecApprovalConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import {
|
||||
@@ -30,7 +31,6 @@ import type {
|
||||
PluginApprovalResolved,
|
||||
} from "openclaw/plugin-sdk/infra-runtime";
|
||||
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { compileSafeRegex, testRegexWithBoundedInput } from "openclaw/plugin-sdk/security-runtime";
|
||||
import { logDebug, logError } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { createDiscordNativeApprovalAdapter } from "../approval-native.js";
|
||||
import { createDiscordClient, stripUndefinedFields } from "../send.shared.js";
|
||||
@@ -190,14 +190,6 @@ class ExecApprovalActionRow extends Row<Button> {
|
||||
}
|
||||
}
|
||||
|
||||
function resolveApprovalAgentId(request: ApprovalRequest): string | null {
|
||||
return request.request.agentId?.trim() || null;
|
||||
}
|
||||
|
||||
function resolveApprovalSessionKey(request: ApprovalRequest): string | null {
|
||||
return request.request.sessionKey?.trim() || null;
|
||||
}
|
||||
|
||||
function buildExecApprovalMetadataLines(request: ExecApprovalRequest): string[] {
|
||||
const lines: string[] = [];
|
||||
if (request.request.cwd) {
|
||||
@@ -491,36 +483,11 @@ export class DiscordExecApprovalHandler {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check agent filter
|
||||
if (config.agentFilter?.length) {
|
||||
const agentId = resolveApprovalAgentId(request);
|
||||
if (!agentId) {
|
||||
return false;
|
||||
}
|
||||
if (!config.agentFilter.includes(agentId)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check session filter (substring match)
|
||||
if (config.sessionFilter?.length) {
|
||||
const session = resolveApprovalSessionKey(request);
|
||||
if (!session) {
|
||||
return false;
|
||||
}
|
||||
const matches = config.sessionFilter.some((p) => {
|
||||
if (session.includes(p)) {
|
||||
return true;
|
||||
}
|
||||
const regex = compileSafeRegex(p);
|
||||
return regex ? testRegexWithBoundedInput(regex, session) : false;
|
||||
});
|
||||
if (!matches) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return matchesApprovalRequestFilters({
|
||||
request: request.request,
|
||||
agentFilter: config.agentFilter,
|
||||
sessionFilter: config.sessionFilter,
|
||||
});
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
doesApprovalRequestMatchChannelAccount,
|
||||
matchesApprovalRequestFilters,
|
||||
resolveApprovalApprovers,
|
||||
} from "openclaw/plugin-sdk/approval-runtime";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
@@ -26,20 +27,6 @@ export function normalizeSlackApproverId(value: string | number): string | undef
|
||||
return /^[UW][A-Z0-9]+$/i.test(trimmed) ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function matchesSlackApprovalSessionFilter(sessionKey: string, patterns: string[]): boolean {
|
||||
const boundedSessionKey = sessionKey.slice(0, 2048);
|
||||
return patterns.some((pattern) => {
|
||||
if (boundedSessionKey.includes(pattern)) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
return new RegExp(pattern).test(boundedSessionKey);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function shouldHandleSlackExecApprovalRequest(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
@@ -62,19 +49,11 @@ export function shouldHandleSlackExecApprovalRequest(params: {
|
||||
if (getSlackExecApprovalApprovers(params).length === 0) {
|
||||
return false;
|
||||
}
|
||||
if (config.agentFilter?.length) {
|
||||
const agentId = params.request.request.agentId?.trim();
|
||||
if (!agentId || !config.agentFilter.includes(agentId)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (config.sessionFilter?.length) {
|
||||
const sessionKey = params.request.request.sessionKey?.trim();
|
||||
if (!sessionKey || !matchesSlackApprovalSessionFilter(sessionKey, config.sessionFilter)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return matchesApprovalRequestFilters({
|
||||
request: params.request.request,
|
||||
agentFilter: config.agentFilter,
|
||||
sessionFilter: config.sessionFilter,
|
||||
});
|
||||
}
|
||||
|
||||
export function getSlackExecApprovalApprovers(params: {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { buildPluginApprovalPendingReplyPayload } from "openclaw/plugin-sdk/approval-runtime";
|
||||
import {
|
||||
buildPluginApprovalPendingReplyPayload,
|
||||
matchesApprovalRequestFilters,
|
||||
} from "openclaw/plugin-sdk/approval-runtime";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import {
|
||||
createExecApprovalChannelRuntime,
|
||||
@@ -17,10 +20,9 @@ import type {
|
||||
PluginApprovalRequest,
|
||||
PluginApprovalResolved,
|
||||
} from "openclaw/plugin-sdk/infra-runtime";
|
||||
import { parseAgentSessionKey, normalizeAccountId } from "openclaw/plugin-sdk/routing";
|
||||
import { normalizeAccountId } from "openclaw/plugin-sdk/routing";
|
||||
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
|
||||
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { compileSafeRegex, testRegexWithBoundedInput } from "openclaw/plugin-sdk/security-runtime";
|
||||
import { telegramNativeApprovalAdapter } from "./approval-native.js";
|
||||
import { resolveTelegramInlineButtons } from "./button-types.js";
|
||||
import {
|
||||
@@ -130,29 +132,15 @@ function matchesFilters(params: {
|
||||
if (approvers.length === 0) {
|
||||
return false;
|
||||
}
|
||||
if (config.agentFilter?.length) {
|
||||
const agentId =
|
||||
params.request.request.agentId ??
|
||||
parseAgentSessionKey(params.request.request.sessionKey)?.agentId;
|
||||
if (!agentId || !config.agentFilter.includes(agentId)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (config.sessionFilter?.length) {
|
||||
const sessionKey = params.request.request.sessionKey;
|
||||
if (!sessionKey) {
|
||||
return false;
|
||||
}
|
||||
const matches = config.sessionFilter.some((pattern) => {
|
||||
if (sessionKey.includes(pattern)) {
|
||||
return true;
|
||||
}
|
||||
const regex = compileSafeRegex(pattern);
|
||||
return regex ? testRegexWithBoundedInput(regex, sessionKey) : false;
|
||||
});
|
||||
if (!matches) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!matchesApprovalRequestFilters({
|
||||
request: params.request.request,
|
||||
agentFilter: config.agentFilter,
|
||||
sessionFilter: config.sessionFilter,
|
||||
fallbackAgentIdFromSessionKey: true,
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const boundAccountId = resolveBoundTelegramAccountId({
|
||||
cfg: params.cfg,
|
||||
|
||||
44
src/infra/approval-request-filters.test.ts
Normal file
44
src/infra/approval-request-filters.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
matchesApprovalRequestFilters,
|
||||
matchesApprovalRequestSessionFilter,
|
||||
} from "./approval-request-filters.js";
|
||||
|
||||
describe("approval request filters", () => {
|
||||
it("matches explicit agent ids and session substrings", () => {
|
||||
expect(
|
||||
matchesApprovalRequestFilters({
|
||||
request: {
|
||||
agentId: "ops-agent",
|
||||
sessionKey: "agent:ops-agent:slack:direct:U1:tail",
|
||||
},
|
||||
agentFilter: ["ops-agent"],
|
||||
sessionFilter: ["slack:direct:", "tail$"],
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("can fall back to the session-key agent id", () => {
|
||||
expect(
|
||||
matchesApprovalRequestFilters({
|
||||
request: {
|
||||
sessionKey: "agent:ops-agent:telegram:group:-1001",
|
||||
},
|
||||
agentFilter: ["ops-agent"],
|
||||
fallbackAgentIdFromSessionKey: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
matchesApprovalRequestFilters({
|
||||
request: {
|
||||
sessionKey: "agent:ops-agent:telegram:group:-1001",
|
||||
},
|
||||
agentFilter: ["ops-agent"],
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects unsafe regex patterns in session filters", () => {
|
||||
expect(matchesApprovalRequestSessionFilter(`${"a".repeat(28)}!`, ["(a+)+$"])).toBe(false);
|
||||
});
|
||||
});
|
||||
47
src/infra/approval-request-filters.ts
Normal file
47
src/infra/approval-request-filters.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { parseAgentSessionKey } from "../routing/session-key.js";
|
||||
import { compileSafeRegex, testRegexWithBoundedInput } from "../security/safe-regex.js";
|
||||
|
||||
export type ApprovalRequestFilterInput = {
|
||||
agentId?: string | null;
|
||||
sessionKey?: string | null;
|
||||
};
|
||||
|
||||
export function matchesApprovalRequestSessionFilter(
|
||||
sessionKey: string,
|
||||
patterns: string[],
|
||||
): boolean {
|
||||
return patterns.some((pattern) => {
|
||||
if (sessionKey.includes(pattern)) {
|
||||
return true;
|
||||
}
|
||||
const regex = compileSafeRegex(pattern);
|
||||
return regex ? testRegexWithBoundedInput(regex, sessionKey) : false;
|
||||
});
|
||||
}
|
||||
|
||||
export function matchesApprovalRequestFilters(params: {
|
||||
request: ApprovalRequestFilterInput;
|
||||
agentFilter?: string[];
|
||||
sessionFilter?: string[];
|
||||
fallbackAgentIdFromSessionKey?: boolean;
|
||||
}): boolean {
|
||||
if (params.agentFilter?.length) {
|
||||
const explicitAgentId = params.request.agentId?.trim() || undefined;
|
||||
const sessionAgentId = params.fallbackAgentIdFromSessionKey
|
||||
? (parseAgentSessionKey(params.request.sessionKey)?.agentId ?? undefined)
|
||||
: undefined;
|
||||
const agentId = explicitAgentId ?? sessionAgentId;
|
||||
if (!agentId || !params.agentFilter.includes(agentId)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (params.sessionFilter?.length) {
|
||||
const sessionKey = params.request.sessionKey?.trim();
|
||||
if (!sessionKey || !matchesApprovalRequestSessionFilter(sessionKey, params.sessionFilter)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -13,14 +13,12 @@ import {
|
||||
buildPluginApprovalPendingReplyPayload,
|
||||
buildPluginApprovalResolvedReplyPayload,
|
||||
} from "../plugin-sdk/approval-renderers.js";
|
||||
import { parseAgentSessionKey } from "../routing/session-key.js";
|
||||
import { compileConfigRegex } from "../security/config-regex.js";
|
||||
import { testRegexWithBoundedInput } from "../security/safe-regex.js";
|
||||
import {
|
||||
isDeliverableMessageChannel,
|
||||
normalizeMessageChannel,
|
||||
type DeliverableMessageChannel,
|
||||
} from "../utils/message-channel.js";
|
||||
import { matchesApprovalRequestFilters } from "./approval-request-filters.js";
|
||||
import { resolveExecApprovalCommandDisplay } from "./exec-approval-command-display.js";
|
||||
import { formatExecApprovalExpiresIn } from "./exec-approval-reply.js";
|
||||
import { resolveExecApprovalSessionTarget } from "./exec-approval-session-target.js";
|
||||
@@ -122,16 +120,6 @@ function normalizeMode(mode?: ExecApprovalForwardingConfig["mode"]) {
|
||||
return mode ?? DEFAULT_MODE;
|
||||
}
|
||||
|
||||
function matchSessionFilter(sessionKey: string, patterns: string[]): boolean {
|
||||
return patterns.some((pattern) => {
|
||||
if (sessionKey.includes(pattern)) {
|
||||
return true;
|
||||
}
|
||||
const compiled = compileConfigRegex(pattern);
|
||||
return compiled?.regex ? testRegexWithBoundedInput(compiled.regex, sessionKey) : false;
|
||||
});
|
||||
}
|
||||
|
||||
function shouldForwardRoute(params: {
|
||||
config?: {
|
||||
enabled?: boolean;
|
||||
@@ -144,20 +132,12 @@ function shouldForwardRoute(params: {
|
||||
if (!config?.enabled) {
|
||||
return false;
|
||||
}
|
||||
if (config.agentFilter?.length) {
|
||||
const agentId =
|
||||
params.routeRequest.agentId ?? parseAgentSessionKey(params.routeRequest.sessionKey)?.agentId;
|
||||
if (!agentId || !config.agentFilter.includes(agentId)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (config.sessionFilter?.length) {
|
||||
const sessionKey = params.routeRequest.sessionKey;
|
||||
if (!sessionKey || !matchSessionFilter(sessionKey, config.sessionFilter)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return matchesApprovalRequestFilters({
|
||||
request: params.routeRequest,
|
||||
agentFilter: config.agentFilter,
|
||||
sessionFilter: config.sessionFilter,
|
||||
fallbackAgentIdFromSessionKey: true,
|
||||
});
|
||||
}
|
||||
|
||||
function buildTargetKey(target: ExecApprovalForwardTarget): string {
|
||||
|
||||
@@ -37,6 +37,11 @@ export {
|
||||
export { createResolvedApproverActionAuthAdapter } from "./approval-auth-helpers.js";
|
||||
export { createApproverRestrictedNativeApprovalAdapter } from "./approval-delivery-helpers.js";
|
||||
export { resolveApprovalApprovers } from "./approval-approvers.js";
|
||||
export {
|
||||
matchesApprovalRequestFilters,
|
||||
matchesApprovalRequestSessionFilter,
|
||||
type ApprovalRequestFilterInput,
|
||||
} from "../infra/approval-request-filters.js";
|
||||
export {
|
||||
buildApprovalPendingReplyPayload,
|
||||
buildApprovalResolvedReplyPayload,
|
||||
|
||||
Reference in New Issue
Block a user