mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-03 13:22:14 +00:00
feat(approvals): auto-enable native chat approvals
This commit is contained in:
@@ -31477,7 +31477,7 @@
|
||||
"network"
|
||||
],
|
||||
"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.",
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@@ -31545,7 +31545,7 @@
|
||||
"network"
|
||||
],
|
||||
"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.",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
@@ -35444,7 +35444,7 @@
|
||||
"network"
|
||||
],
|
||||
"label": "Telegram Exec Approvals",
|
||||
"help": "Telegram-native exec approval routing and approver authorization. Enable this only when Telegram should act as an explicit exec-approval client for the selected bot account.",
|
||||
"help": "Telegram-native exec approval routing and approver authorization. When unset, OpenClaw auto-enables DM-first native approvals if approvers can be resolved for the selected bot account.",
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
@@ -35512,7 +35512,7 @@
|
||||
"network"
|
||||
],
|
||||
"label": "Telegram Exec Approvals Enabled",
|
||||
"help": "Enable Telegram exec approvals for this account. When false or unset, Telegram messages/buttons cannot approve exec requests.",
|
||||
"help": "Controls Telegram 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.",
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2830,12 +2830,12 @@
|
||||
{"recordType":"path","path":"channels.slack.dms.*","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.slack.dms.*.historyLimit","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.execApprovals","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"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.","hasChildren":true}
|
||||
{"recordType":"path","path":"channels.slack.execApprovals","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Slack Exec Approvals","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.","hasChildren":true}
|
||||
{"recordType":"path","path":"channels.slack.execApprovals.agentFilter","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Slack Exec Approval Agent Filter","help":"Optional allowlist of agent IDs eligible for Slack exec approvals, for example `[\"main\", \"ops-agent\"]`. Use this to keep approval prompts scoped to the agents you actually operate from Slack.","hasChildren":true}
|
||||
{"recordType":"path","path":"channels.slack.execApprovals.agentFilter.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.execApprovals.approvers","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Slack Exec Approval Approvers","help":"Slack user IDs allowed to approve exec requests for this workspace account. Use Slack user IDs or user targets such as `U123`, `user:U123`, or `<@U123>`. If you leave this unset, OpenClaw falls back to commands.ownerAllowFrom when possible.","hasChildren":true}
|
||||
{"recordType":"path","path":"channels.slack.execApprovals.approvers.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.execApprovals.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Slack Exec Approvals Enabled","help":"Enable Slack exec approvals for this account. When false or unset, Slack messages/buttons cannot approve exec requests.","hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.execApprovals.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Slack Exec Approvals Enabled","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.","hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.execApprovals.sessionFilter","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network","storage"],"label":"Slack Exec Approval Session Filter","help":"Optional session-key filters matched as substring or regex-style patterns before Slack approval routing is used. Use narrow patterns so Slack approvals only appear for intended sessions.","hasChildren":true}
|
||||
{"recordType":"path","path":"channels.slack.execApprovals.sessionFilter.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.execApprovals.target","kind":"channel","type":"string","required":false,"enumValues":["dm","channel","both"],"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Slack Exec Approval Target","help":"Controls where Slack approval prompts are sent: \"dm\" sends to approver DMs (default), \"channel\" sends to the originating Slack chat/thread, and \"both\" sends to both. Channel delivery exposes the command text to the chat, so only use it in trusted channels.","hasChildren":false}
|
||||
@@ -3182,12 +3182,12 @@
|
||||
{"recordType":"path","path":"channels.telegram.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.errorCooldownMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.errorPolicy","kind":"channel","type":"string","required":false,"enumValues":["always","once","silent"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.execApprovals","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Telegram Exec Approvals","help":"Telegram-native exec approval routing and approver authorization. Enable this only when Telegram should act as an explicit exec-approval client for the selected bot account.","hasChildren":true}
|
||||
{"recordType":"path","path":"channels.telegram.execApprovals","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Telegram Exec Approvals","help":"Telegram-native exec approval routing and approver authorization. When unset, OpenClaw auto-enables DM-first native approvals if approvers can be resolved for the selected bot account.","hasChildren":true}
|
||||
{"recordType":"path","path":"channels.telegram.execApprovals.agentFilter","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Telegram Exec Approval Agent Filter","help":"Optional allowlist of agent IDs eligible for Telegram exec approvals, for example `[\"main\", \"ops-agent\"]`. Use this to keep approval prompts scoped to the agents you actually operate from Telegram.","hasChildren":true}
|
||||
{"recordType":"path","path":"channels.telegram.execApprovals.agentFilter.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.execApprovals.approvers","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Telegram Exec Approval Approvers","help":"Telegram user IDs allowed to approve exec requests for this bot account. Use numeric Telegram user IDs. If you leave this unset, OpenClaw falls back to numeric owner IDs inferred from channels.telegram.allowFrom and direct-message defaultTo when possible.","hasChildren":true}
|
||||
{"recordType":"path","path":"channels.telegram.execApprovals.approvers.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.execApprovals.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Telegram Exec Approvals Enabled","help":"Enable Telegram exec approvals for this account. When false or unset, Telegram messages/buttons cannot approve exec requests.","hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.execApprovals.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Telegram Exec Approvals Enabled","help":"Controls Telegram 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.","hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.execApprovals.sessionFilter","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network","storage"],"label":"Telegram Exec Approval Session Filter","help":"Optional session-key filters matched as substring or regex-style patterns before Telegram approval routing is used. Use narrow patterns so Telegram approvals only appear for intended sessions.","hasChildren":true}
|
||||
{"recordType":"path","path":"channels.telegram.execApprovals.sessionFilter.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.execApprovals.target","kind":"channel","type":"string","required":false,"enumValues":["dm","channel","both"],"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Telegram Exec Approval Target","help":"Controls where Telegram approval prompts are sent: \"dm\" sends to approver DMs (default), \"channel\" sends to the originating Telegram chat/topic, and \"both\" sends to both. Channel delivery exposes the command text to the chat, so only use it in trusted groups/topics.","hasChildren":false}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" },
|
||||
);
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -83,11 +83,11 @@ export const telegramChannelConfigUiHints = {
|
||||
},
|
||||
execApprovals: {
|
||||
label: "Telegram Exec Approvals",
|
||||
help: "Telegram-native exec approval routing and approver authorization. Enable this only when Telegram should act as an explicit exec-approval client for the selected bot account.",
|
||||
help: "Telegram-native exec approval routing and approver authorization. When unset, OpenClaw auto-enables DM-first native approvals if approvers can be resolved for the selected bot account.",
|
||||
},
|
||||
"execApprovals.enabled": {
|
||||
label: "Telegram Exec Approvals Enabled",
|
||||
help: "Enable Telegram exec approvals for this account. When false or unset, Telegram messages/buttons cannot approve exec requests.",
|
||||
help: 'Controls Telegram 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: "Telegram Exec Approval Approvers",
|
||||
|
||||
@@ -21,8 +21,7 @@ import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { telegramNativeApprovalAdapter } from "./approval-native.js";
|
||||
import { resolveTelegramInlineButtons } from "./button-types.js";
|
||||
import {
|
||||
getTelegramExecApprovalApprovers,
|
||||
resolveTelegramExecApprovalConfig,
|
||||
isTelegramExecApprovalHandlerConfigured,
|
||||
shouldHandleTelegramExecApprovalRequest,
|
||||
} from "./exec-approvals.js";
|
||||
import { editMessageReplyMarkupTelegram, sendMessageTelegram, sendTypingTelegram } from "./send.js";
|
||||
@@ -56,19 +55,7 @@ export type TelegramExecApprovalHandlerDeps = {
|
||||
};
|
||||
|
||||
function isHandlerConfigured(params: { cfg: OpenClawConfig; accountId: string }): boolean {
|
||||
const config = resolveTelegramExecApprovalConfig({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
if (!config?.enabled) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
getTelegramExecApprovalApprovers({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
}).length > 0
|
||||
);
|
||||
return isTelegramExecApprovalHandlerConfigured(params);
|
||||
}
|
||||
|
||||
export class TelegramExecApprovalHandler {
|
||||
|
||||
@@ -28,7 +28,7 @@ function buildConfig(
|
||||
}
|
||||
|
||||
describe("telegram exec approvals", () => {
|
||||
it("requires enablement and an explicit or inferred approver", () => {
|
||||
it("auto-enables when approvers resolve and disables only when forced off", () => {
|
||||
expect(isTelegramExecApprovalClientEnabled({ cfg: buildConfig() })).toBe(false);
|
||||
expect(
|
||||
isTelegramExecApprovalClientEnabled({
|
||||
@@ -37,18 +37,23 @@ describe("telegram exec approvals", () => {
|
||||
).toBe(false);
|
||||
expect(
|
||||
isTelegramExecApprovalClientEnabled({
|
||||
cfg: buildConfig({ enabled: true }, { allowFrom: ["123"] }),
|
||||
cfg: buildConfig(undefined, { allowFrom: ["123"] }),
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isTelegramExecApprovalClientEnabled({
|
||||
cfg: buildConfig({ enabled: true, approvers: ["123"] }),
|
||||
cfg: buildConfig({ approvers: ["123"] }),
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isTelegramExecApprovalClientEnabled({
|
||||
cfg: buildConfig({ enabled: false, approvers: ["123"] }),
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("matches approvers by normalized sender id", () => {
|
||||
const cfg = buildConfig({ enabled: true, approvers: [123, "456"] });
|
||||
const cfg = buildConfig({ approvers: [123, "456"] });
|
||||
expect(isTelegramExecApprovalApprover({ cfg, senderId: "123" })).toBe(true);
|
||||
expect(isTelegramExecApprovalApprover({ cfg, senderId: "456" })).toBe(true);
|
||||
expect(isTelegramExecApprovalApprover({ cfg, senderId: "789" })).toBe(false);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
createChannelExecApprovalProfile,
|
||||
isChannelExecApprovalClientEnabledFromConfig,
|
||||
isChannelExecApprovalTargetRecipient,
|
||||
resolveApprovalRequestAccountId,
|
||||
resolveApprovalApprovers,
|
||||
@@ -138,3 +139,13 @@ export function shouldSuppressLocalTelegramExecApprovalPrompt(params: {
|
||||
}): boolean {
|
||||
return telegramExecApprovalProfile.shouldSuppressLocalPrompt(params);
|
||||
}
|
||||
|
||||
export function isTelegramExecApprovalHandlerConfigured(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}): boolean {
|
||||
return isChannelExecApprovalClientEnabledFromConfig({
|
||||
enabled: resolveTelegramExecApprovalConfig(params)?.enabled,
|
||||
approverCount: getTelegramExecApprovalApprovers(params).length,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import { resolveTelegramAccount } from "./accounts.js";
|
||||
import { resolveTelegramAllowedUpdates } from "./allowed-updates.js";
|
||||
import { isTelegramExecApprovalHandlerConfigured } from "./exec-approvals.js";
|
||||
import { resolveTelegramTransport } from "./fetch.js";
|
||||
import {
|
||||
isRecoverableTelegramNetworkError,
|
||||
@@ -145,13 +146,15 @@ export async function monitorTelegramProvider(opts: MonitorTelegramOpts = {}) {
|
||||
if (opts.useWebhook) {
|
||||
const { TelegramExecApprovalHandler, startTelegramWebhook } =
|
||||
await loadTelegramMonitorWebhookRuntime();
|
||||
execApprovalsHandler = new TelegramExecApprovalHandler({
|
||||
token,
|
||||
accountId: account.accountId,
|
||||
cfg,
|
||||
runtime: opts.runtime,
|
||||
});
|
||||
await execApprovalsHandler.start();
|
||||
if (isTelegramExecApprovalHandlerConfigured({ cfg, accountId: account.accountId })) {
|
||||
execApprovalsHandler = new TelegramExecApprovalHandler({
|
||||
token,
|
||||
accountId: account.accountId,
|
||||
cfg,
|
||||
runtime: opts.runtime,
|
||||
});
|
||||
await execApprovalsHandler.start();
|
||||
}
|
||||
await startTelegramWebhook({
|
||||
token,
|
||||
accountId: account.accountId,
|
||||
@@ -177,13 +180,15 @@ export async function monitorTelegramProvider(opts: MonitorTelegramOpts = {}) {
|
||||
writeTelegramUpdateOffset,
|
||||
} = await loadTelegramMonitorPollingRuntime();
|
||||
|
||||
execApprovalsHandler = new TelegramExecApprovalHandler({
|
||||
token,
|
||||
accountId: account.accountId,
|
||||
cfg,
|
||||
runtime: opts.runtime,
|
||||
});
|
||||
await execApprovalsHandler.start();
|
||||
if (isTelegramExecApprovalHandlerConfigured({ cfg, accountId: account.accountId })) {
|
||||
execApprovalsHandler = new TelegramExecApprovalHandler({
|
||||
token,
|
||||
accountId: account.accountId,
|
||||
cfg,
|
||||
runtime: opts.runtime,
|
||||
});
|
||||
await execApprovalsHandler.start();
|
||||
}
|
||||
|
||||
const persistedOffsetRaw = await readTelegramUpdateOffset({
|
||||
accountId: account.accountId,
|
||||
|
||||
@@ -11566,11 +11566,11 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
},
|
||||
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",
|
||||
@@ -13721,11 +13721,11 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
},
|
||||
execApprovals: {
|
||||
label: "Telegram Exec Approvals",
|
||||
help: "Telegram-native exec approval routing and approver authorization. Enable this only when Telegram should act as an explicit exec-approval client for the selected bot account.",
|
||||
help: "Telegram-native exec approval routing and approver authorization. When unset, OpenClaw auto-enables DM-first native approvals if approvers can be resolved for the selected bot account.",
|
||||
},
|
||||
"execApprovals.enabled": {
|
||||
label: "Telegram Exec Approvals Enabled",
|
||||
help: "Enable Telegram exec approvals for this account. When false or unset, Telegram messages/buttons cannot approve exec requests.",
|
||||
help: 'Controls Telegram 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: "Telegram Exec Approval Approvers",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export type NativeExecApprovalEnableMode = boolean | "auto";
|
||||
|
||||
export type ExecApprovalForwardingMode = "session" | "targets" | "both";
|
||||
|
||||
export type ExecApprovalForwardTarget = {
|
||||
|
||||
@@ -140,8 +140,8 @@ export type DiscordVoiceConfig = {
|
||||
};
|
||||
|
||||
export type DiscordExecApprovalConfig = {
|
||||
/** Enable exec approval forwarding to Discord DMs. Default: false. */
|
||||
enabled?: boolean;
|
||||
/** Enable mode for Discord exec approvals on this account. Default: auto when approvers can be resolved; false disables. */
|
||||
enabled?: import("./types.approvals.js").NativeExecApprovalEnableMode;
|
||||
/** Discord user IDs to receive approval prompts. Optional: falls back to commands.ownerAllowFrom when possible. */
|
||||
approvers?: string[];
|
||||
/** Only forward approvals for these agent IDs. Omit = all agents. */
|
||||
|
||||
@@ -52,8 +52,8 @@ export type SlackStreamingMode = "off" | "partial" | "block" | "progress";
|
||||
export type SlackLegacyStreamMode = "replace" | "status_final" | "append";
|
||||
export type SlackExecApprovalTarget = "dm" | "channel" | "both";
|
||||
export type SlackExecApprovalConfig = {
|
||||
/** Enable Slack exec approvals for this account. Default: false. */
|
||||
enabled?: boolean;
|
||||
/** Enable mode for Slack exec approvals on this account. Default: auto when approvers can be resolved; false disables. */
|
||||
enabled?: import("./types.approvals.js").NativeExecApprovalEnableMode;
|
||||
/** Slack user IDs allowed to approve exec requests. Optional: falls back to commands.ownerAllowFrom when possible. */
|
||||
approvers?: Array<string | number>;
|
||||
/** Only forward approvals for these agent IDs. Omit = all agents. */
|
||||
|
||||
@@ -59,8 +59,8 @@ export type TelegramStreamingMode = "off" | "partial" | "block" | "progress";
|
||||
export type TelegramExecApprovalTarget = "dm" | "channel" | "both";
|
||||
|
||||
export type TelegramExecApprovalConfig = {
|
||||
/** Enable Telegram exec approvals for this account. Default: false. */
|
||||
enabled?: boolean;
|
||||
/** Enable mode for Telegram exec approvals on this account. Default: auto when approvers can be resolved; false disables. */
|
||||
enabled?: import("./types.approvals.js").NativeExecApprovalEnableMode;
|
||||
/** Telegram user IDs allowed to approve exec requests. Optional: falls back to numeric owner IDs inferred from allowFrom/defaultTo when possible. */
|
||||
approvers?: Array<string | number>;
|
||||
/** Only forward approvals for these agent IDs. Omit = all agents. */
|
||||
|
||||
@@ -54,12 +54,12 @@ describe("exec approval reply helpers", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("mentions Slack in the fallback approval-client guidance", () => {
|
||||
it("mentions native chat approval clients in the fallback guidance", () => {
|
||||
expect(
|
||||
buildExecApprovalUnavailableReplyPayload({
|
||||
reason: "no-approval-route",
|
||||
}).text,
|
||||
).toContain("Discord, Slack, or Telegram exec approvals");
|
||||
).toContain("native chat approval client such as Discord, Slack, or Telegram");
|
||||
});
|
||||
|
||||
it.each(invalidReplyMetadataCases)(
|
||||
|
||||
@@ -324,21 +324,21 @@ export function buildExecApprovalUnavailableReplyPayload(
|
||||
`Exec approval is required, but chat exec approvals are not enabled on ${params.channelLabel ?? "this platform"}.`,
|
||||
);
|
||||
lines.push(
|
||||
"Approve it from the Web UI or terminal UI, or enable Discord, Slack, or Telegram exec approvals. If those accounts already know your owner ID via allowFrom, OpenClaw can infer approvers automatically.",
|
||||
"Approve it from the Web UI or terminal UI, or enable a native chat approval client such as Discord, Slack, or Telegram. If those accounts already know your owner ID via allowFrom or owner config, OpenClaw can often infer approvers automatically.",
|
||||
);
|
||||
} else if (params.reason === "initiating-platform-unsupported") {
|
||||
lines.push(
|
||||
`Exec approval is required, but ${params.channelLabel ?? "this platform"} does not support chat exec approvals.`,
|
||||
);
|
||||
lines.push(
|
||||
"Approve it from the Web UI or terminal UI, or enable Discord, Slack, or Telegram exec approvals. If those accounts already know your owner ID via allowFrom, OpenClaw can infer approvers automatically.",
|
||||
"Approve it from the Web UI or terminal UI, or enable a native chat approval client such as Discord, Slack, or Telegram. If those accounts already know your owner ID via allowFrom or owner config, OpenClaw can often infer approvers automatically.",
|
||||
);
|
||||
} else {
|
||||
lines.push(
|
||||
"Exec approval is required, but no interactive approval client is currently available.",
|
||||
);
|
||||
lines.push(
|
||||
"Open the Web UI or terminal UI, or enable Discord, Slack, or Telegram exec approvals, then retry the command. If those accounts already know your owner ID via allowFrom, you can usually leave execApprovals.approvers unset.",
|
||||
"Open the Web UI or terminal UI, or enable a native chat approval client such as Discord, Slack, or Telegram, then retry the command. If those accounts already know your owner ID via allowFrom or owner config, you can usually leave execApprovals.approvers unset.",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
createChannelExecApprovalProfile,
|
||||
isChannelExecApprovalClientEnabledFromConfig,
|
||||
isChannelExecApprovalTargetRecipient,
|
||||
} from "./approval-client-helpers.js";
|
||||
import type { OpenClawConfig } from "./config-runtime.js";
|
||||
@@ -76,6 +77,37 @@ describe("createChannelExecApprovalProfile", () => {
|
||||
matchesRequestAccount: ({ accountId }) => accountId !== "other",
|
||||
});
|
||||
|
||||
it("treats unset enabled as auto and false as disabled", () => {
|
||||
expect(
|
||||
isChannelExecApprovalClientEnabledFromConfig({
|
||||
approverCount: 1,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isChannelExecApprovalClientEnabledFromConfig({
|
||||
enabled: "auto",
|
||||
approverCount: 1,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isChannelExecApprovalClientEnabledFromConfig({
|
||||
enabled: true,
|
||||
approverCount: 1,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isChannelExecApprovalClientEnabledFromConfig({
|
||||
enabled: false,
|
||||
approverCount: 1,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isChannelExecApprovalClientEnabledFromConfig({
|
||||
approverCount: 0,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("reuses shared client, auth, and request-filter logic", () => {
|
||||
expect(profile.isClientEnabled({ cfg: {} })).toBe(true);
|
||||
expect(profile.isApprover({ cfg: {}, senderId: "owner" })).toBe(true);
|
||||
|
||||
@@ -9,9 +9,10 @@ import { normalizeAccountId } from "./routing.js";
|
||||
|
||||
type ApprovalRequest = ExecApprovalRequest | PluginApprovalRequest;
|
||||
type ApprovalTarget = "dm" | "channel" | "both";
|
||||
type ChannelExecApprovalEnableMode = boolean | "auto";
|
||||
|
||||
type ChannelApprovalConfig = {
|
||||
enabled?: boolean;
|
||||
enabled?: ChannelExecApprovalEnableMode;
|
||||
target?: ApprovalTarget;
|
||||
agentFilter?: string[];
|
||||
sessionFilter?: string[];
|
||||
@@ -35,6 +36,16 @@ function isApprovalTargetsMode(cfg: OpenClawConfig): boolean {
|
||||
return execApprovals.mode === "targets" || execApprovals.mode === "both";
|
||||
}
|
||||
|
||||
export function isChannelExecApprovalClientEnabledFromConfig(params: {
|
||||
enabled?: ChannelExecApprovalEnableMode;
|
||||
approverCount: number;
|
||||
}): boolean {
|
||||
if (params.approverCount <= 0) {
|
||||
return false;
|
||||
}
|
||||
return params.enabled !== false;
|
||||
}
|
||||
|
||||
export function isChannelExecApprovalTargetRecipient(params: {
|
||||
cfg: OpenClawConfig;
|
||||
senderId?: string | null;
|
||||
@@ -91,7 +102,10 @@ export function createChannelExecApprovalProfile(params: {
|
||||
|
||||
const isClientEnabled = (input: ApprovalProfileParams): boolean => {
|
||||
const config = params.resolveConfig(input);
|
||||
return Boolean(config?.enabled && params.resolveApprovers(input).length > 0);
|
||||
return isChannelExecApprovalClientEnabledFromConfig({
|
||||
enabled: config?.enabled,
|
||||
approverCount: params.resolveApprovers(input).length,
|
||||
});
|
||||
};
|
||||
|
||||
const isApprover = (input: ApprovalProfileParams & { senderId?: string | null }): boolean => {
|
||||
@@ -119,16 +133,19 @@ export function createChannelExecApprovalProfile(params: {
|
||||
return false;
|
||||
}
|
||||
const config = params.resolveConfig(input);
|
||||
if (!config?.enabled) {
|
||||
return false;
|
||||
}
|
||||
if (params.resolveApprovers(input).length === 0) {
|
||||
const approverCount = params.resolveApprovers(input).length;
|
||||
if (
|
||||
!isChannelExecApprovalClientEnabledFromConfig({
|
||||
enabled: config?.enabled,
|
||||
approverCount,
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return matchesApprovalRequestFilters({
|
||||
request: input.request.request,
|
||||
agentFilter: config.agentFilter,
|
||||
sessionFilter: config.sessionFilter,
|
||||
agentFilter: config?.agentFilter,
|
||||
sessionFilter: config?.sessionFilter,
|
||||
fallbackAgentIdFromSessionKey: params.fallbackAgentIdFromSessionKey === true,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -40,6 +40,7 @@ export {
|
||||
export { createResolvedApproverActionAuthAdapter } from "./approval-auth-helpers.js";
|
||||
export {
|
||||
createChannelExecApprovalProfile,
|
||||
isChannelExecApprovalClientEnabledFromConfig,
|
||||
isChannelExecApprovalTargetRecipient,
|
||||
} from "./approval-client-helpers.js";
|
||||
export {
|
||||
|
||||
Reference in New Issue
Block a user