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

@@ -51,11 +51,11 @@ export const slackChannelConfigUiHints = {
},
execApprovals: {
label: "Slack Exec Approvals",
help: "Slack-native exec approval routing and approver authorization. Enable this only when Slack should act as an explicit exec-approval client for the selected workspace account.",
help: "Slack-native exec approval routing and approver authorization. When unset, OpenClaw auto-enables DM-first native approvals if approvers can be resolved for this workspace account.",
},
"execApprovals.enabled": {
label: "Slack Exec Approvals Enabled",
help: "Enable Slack exec approvals for this account. When false or unset, Slack messages/buttons cannot approve exec requests.",
help: 'Controls Slack native exec approvals for this account: unset or "auto" enables DM-first native approvals when approvers can be resolved, true forces native approvals on, and false disables them.',
},
"execApprovals.approvers": {
label: "Slack Exec Approval Approvers",

View File

@@ -29,32 +29,36 @@ function buildConfig(
}
describe("slack exec approvals", () => {
it("requires enablement and explicit or owner approvers", () => {
it("auto-enables when owner approvers resolve and disables only when forced off", () => {
expect(isSlackExecApprovalClientEnabled({ cfg: buildConfig() })).toBe(false);
expect(isSlackExecApprovalClientEnabled({ cfg: buildConfig({ enabled: true }) })).toBe(false);
expect(
isSlackExecApprovalClientEnabled({
cfg: buildConfig({ enabled: true }, { allowFrom: ["U123"] }),
cfg: buildConfig({ enabled: true }),
}),
).toBe(false);
expect(
isSlackExecApprovalClientEnabled({
cfg: buildConfig({ enabled: true, approvers: ["U123"] }),
cfg: buildConfig({ approvers: ["U123"] }),
}),
).toBe(true);
expect(
isSlackExecApprovalClientEnabled({
cfg: {
...buildConfig({ enabled: true }),
...buildConfig(),
commands: { ownerAllowFrom: ["slack:U123OWNER"] },
} as OpenClawConfig,
}),
).toBe(true);
expect(
isSlackExecApprovalClientEnabled({
cfg: buildConfig({ enabled: false, approvers: ["U123"] }),
}),
).toBe(false);
});
it("prefers explicit approvers when configured", () => {
const cfg = buildConfig(
{ enabled: true, approvers: ["U456"] },
{ approvers: ["U456"] },
{ allowFrom: ["U123"], defaultTo: "user:U789" },
);

View File

@@ -16,6 +16,7 @@ import { logError } from "openclaw/plugin-sdk/text-runtime";
import { slackNativeApprovalAdapter } from "../approval-native.js";
import {
getSlackExecApprovalApprovers,
isSlackExecApprovalClientEnabled,
normalizeSlackApproverId,
shouldHandleSlackExecApprovalRequest,
} from "../exec-approvals.js";
@@ -239,13 +240,10 @@ export class SlackExecApprovalHandler {
gatewayUrl: opts.gatewayUrl,
nativeAdapter: slackNativeApprovalAdapter.native,
isConfigured: () =>
Boolean(
opts.config.enabled &&
getSlackExecApprovalApprovers({
cfg: opts.cfg,
accountId: opts.accountId,
}).length > 0,
),
isSlackExecApprovalClientEnabled({
cfg: opts.cfg,
accountId: opts.accountId,
}),
shouldHandle: (request) => this.shouldHandle(request),
buildPendingContent: ({ request }) => ({
text: buildSlackPendingApprovalText(request),

View File

@@ -31,6 +31,7 @@ import { normalizeStringEntries } from "openclaw/plugin-sdk/text-runtime";
import { installRequestBodyLimitGuard } from "openclaw/plugin-sdk/webhook-request-guards";
import { resolveSlackAccount } from "../accounts.js";
import { resolveSlackWebClientOptions } from "../client.js";
import { isSlackExecApprovalClientEnabled } from "../exec-approvals.js";
import { normalizeSlackWebhookPath, registerSlackHttpHandler } from "../http/index.js";
import { SLACK_TEXT_LIMIT } from "../limits.js";
import { resolveSlackChannelAllowlist, type SlackChannelResolution } from "../resolve-channels.js";
@@ -406,11 +407,14 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
: undefined;
const handleSlackMessage = createSlackMessageHandler({ ctx, account, trackEvent });
const execApprovalsHandler = slackCfg.execApprovals?.enabled
const execApprovalsHandler = isSlackExecApprovalClientEnabled({
cfg,
accountId: account.accountId,
})
? new SlackExecApprovalHandler({
app,
accountId: account.accountId,
config: slackCfg.execApprovals,
config: slackCfg.execApprovals ?? {},
cfg,
})
: null;