feat(approvals): auto-enable native chat approvals

This commit is contained in:
Peter Steinberger
2026-04-02 17:28:32 +01:00
parent 721cab2b8d
commit 17f6626ffe
26 changed files with 197 additions and 113 deletions

View File

@@ -4,6 +4,7 @@ import {
createApproverRestrictedNativeApprovalCapability,
splitChannelApprovalCapability,
doesApprovalRequestMatchChannelAccount,
isChannelExecApprovalClientEnabledFromConfig,
matchesApprovalRequestFilters,
} from "openclaw/plugin-sdk/approval-runtime";
import type { DiscordExecApprovalConfig, OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
@@ -75,16 +76,18 @@ export function shouldHandleDiscordApprovalRequest(params: {
) {
return false;
}
if (!config) {
return true;
}
if (!config.enabled || approvers.length === 0) {
if (
!isChannelExecApprovalClientEnabledFromConfig({
enabled: config?.enabled,
approverCount: approvers.length,
})
) {
return false;
}
return matchesApprovalRequestFilters({
request: params.request.request,
agentFilter: config.agentFilter,
sessionFilter: config.sessionFilter,
agentFilter: config?.agentFilter,
sessionFilter: config?.sessionFilter,
});
}

View File

@@ -22,34 +22,35 @@ function buildConfig(
}
describe("discord exec approvals", () => {
it("requires enablement and explicit or owner approvers", () => {
it("auto-enables when owner approvers resolve and disables only when forced off", () => {
expect(isDiscordExecApprovalClientEnabled({ cfg: buildConfig() })).toBe(false);
expect(isDiscordExecApprovalClientEnabled({ cfg: buildConfig({ enabled: true }) })).toBe(false);
expect(
isDiscordExecApprovalClientEnabled({
cfg: buildConfig({ enabled: true }, { allowFrom: ["123"] }),
cfg: buildConfig({ enabled: true }),
}),
).toBe(false);
expect(
isDiscordExecApprovalClientEnabled({
cfg: buildConfig({ enabled: true, approvers: ["123"] }),
cfg: buildConfig({ approvers: ["123"] }),
}),
).toBe(true);
expect(
isDiscordExecApprovalClientEnabled({
cfg: {
...buildConfig({ enabled: true }),
...buildConfig(),
commands: { ownerAllowFrom: ["discord:789"] },
} as OpenClawConfig,
}),
).toBe(true);
expect(
isDiscordExecApprovalClientEnabled({
cfg: buildConfig({ enabled: false, approvers: ["123"] }),
}),
).toBe(false);
});
it("prefers explicit approvers when configured", () => {
const cfg = buildConfig(
{ enabled: true, approvers: ["456"] },
{ allowFrom: ["123"], defaultTo: "user:789" },
);
const cfg = buildConfig({ approvers: ["456"] }, { allowFrom: ["123"], defaultTo: "user:789" });
expect(getDiscordExecApprovalApprovers({ cfg })).toEqual(["456"]);
expect(isDiscordExecApprovalApprover({ cfg, senderId: "456" })).toBe(true);
@@ -72,7 +73,7 @@ describe("discord exec approvals", () => {
it("falls back to commands.ownerAllowFrom for exec approvers", () => {
const cfg = {
...buildConfig({ enabled: true }),
...buildConfig(),
commands: { ownerAllowFrom: ["discord:123", "user:456", "789"] },
} as OpenClawConfig;

View File

@@ -1,4 +1,5 @@
import { getExecApprovalReplyMetadata } from "openclaw/plugin-sdk/approval-runtime";
import { isChannelExecApprovalClientEnabledFromConfig } from "openclaw/plugin-sdk/approval-runtime";
import { resolveApprovalApprovers } from "openclaw/plugin-sdk/approval-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import type { DiscordExecApprovalConfig } from "openclaw/plugin-sdk/config-runtime";
@@ -53,14 +54,14 @@ export function isDiscordExecApprovalClientEnabled(params: {
configOverride?: DiscordExecApprovalConfig | null;
}): boolean {
const config = params.configOverride ?? resolveDiscordAccount(params).config.execApprovals;
return Boolean(
config?.enabled &&
getDiscordExecApprovalApprovers({
return isChannelExecApprovalClientEnabledFromConfig({
enabled: config?.enabled,
approverCount: getDiscordExecApprovalApprovers({
cfg: params.cfg,
accountId: params.accountId,
configOverride: params.configOverride,
}).length > 0,
);
}).length,
});
}
export function isDiscordExecApprovalApprover(params: {

View File

@@ -34,7 +34,10 @@ import {
createDiscordApprovalCapability,
shouldHandleDiscordApprovalRequest,
} from "../approval-native.js";
import { getDiscordExecApprovalApprovers } from "../exec-approvals.js";
import {
getDiscordExecApprovalApprovers,
isDiscordExecApprovalClientEnabled,
} from "../exec-approvals.js";
import { createDiscordClient, stripUndefinedFields } from "../send.shared.js";
import { DiscordUiContainer } from "../ui.js";
@@ -484,7 +487,12 @@ export class DiscordExecApprovalHandler {
gatewayUrl: this.opts.gatewayUrl,
eventKinds: ["exec", "plugin"],
nativeAdapter: createDiscordApprovalCapability(this.opts.config).native,
isConfigured: () => Boolean(this.opts.config.enabled && this.getApprovers().length > 0),
isConfigured: () =>
isDiscordExecApprovalClientEnabled({
cfg: this.opts.cfg,
accountId: this.opts.accountId,
configOverride: this.opts.config,
}),
shouldHandle: (request) => this.shouldHandle(request),
buildPendingContent: ({ request }) => {
const actionRow = createApprovalActionRow(request);

View File

@@ -20,12 +20,6 @@ import {
} from "openclaw/plugin-sdk/config-runtime";
import type { OpenClawConfig, ReplyToMode } from "openclaw/plugin-sdk/config-runtime";
import { loadConfig } from "openclaw/plugin-sdk/config-runtime";
import {
GROUP_POLICY_BLOCKED_LABEL,
resolveOpenProviderRuntimeGroupPolicy,
resolveDefaultGroupPolicy,
warnMissingProviderGroupPolicyFallbackOnce,
} from "openclaw/plugin-sdk/runtime-group-policy";
import { createConnectedChannelStatusPatch } from "openclaw/plugin-sdk/gateway-runtime";
import { getPluginCommandSpecs } from "openclaw/plugin-sdk/plugin-runtime";
import { resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-runtime";
@@ -38,9 +32,16 @@ import {
} from "openclaw/plugin-sdk/runtime-env";
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
import { createNonExitingRuntime, type RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
import {
GROUP_POLICY_BLOCKED_LABEL,
resolveOpenProviderRuntimeGroupPolicy,
resolveDefaultGroupPolicy,
warnMissingProviderGroupPolicyFallbackOnce,
} from "openclaw/plugin-sdk/runtime-group-policy";
import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime";
import { summarizeStringEntries } from "openclaw/plugin-sdk/text-runtime";
import { resolveDiscordAccount } from "../accounts.js";
import { isDiscordExecApprovalClientEnabled } from "../exec-approvals.js";
import { fetchDiscordApplicationId } from "../probe.js";
import { normalizeDiscordToken } from "../token.js";
import { createDiscordVoiceCommand } from "../voice/command.js";
@@ -824,7 +825,11 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
// Initialize exec approvals handler if enabled
const execApprovalsConfig = discordCfg.execApprovals ?? {};
const execApprovalsHandler = execApprovalsConfig.enabled
const execApprovalsHandler = isDiscordExecApprovalClientEnabled({
cfg,
accountId: account.accountId,
configOverride: execApprovalsConfig,
})
? new DiscordExecApprovalHandler({
token,
accountId: account.accountId,