feat(discord): add exec approval forwarding to DMs (#1621)
* feat(discord): add exec approval forwarding to DMs Add support for forwarding exec approval requests to Discord DMs, allowing users to approve/deny command execution via interactive buttons. Features: - New DiscordExecApprovalHandler that connects to gateway and listens for exec.approval.requested/resolved events - Sends DMs with embeds showing command details and 3 buttons: Allow once, Always allow, Deny - Configurable via channels.discord.execApprovals with: - enabled: boolean - approvers: Discord user IDs to notify - agentFilter: only forward for specific agents - sessionFilter: only forward for matching session patterns - Updates message embed when approval is resolved or expires Also fixes exec completion routing: when async exec completes after approval, the heartbeat now uses a specialized prompt to ensure the model relays the result to the user instead of responding HEARTBEAT_OK. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: generic exec approvals forwarding (#1621) (thanks @czekaj) --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
29
src/config/types.approvals.ts
Normal file
29
src/config/types.approvals.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
export type ExecApprovalForwardingMode = "session" | "targets" | "both";
|
||||
|
||||
export type ExecApprovalForwardTarget = {
|
||||
/** Channel id (e.g. "discord", "slack", or plugin channel id). */
|
||||
channel: string;
|
||||
/** Destination id (channel id, user id, etc. depending on channel). */
|
||||
to: string;
|
||||
/** Optional account id for multi-account channels. */
|
||||
accountId?: string;
|
||||
/** Optional thread id to reply inside a thread. */
|
||||
threadId?: string | number;
|
||||
};
|
||||
|
||||
export type ExecApprovalForwardingConfig = {
|
||||
/** Enable forwarding exec approvals to chat channels. Default: false. */
|
||||
enabled?: boolean;
|
||||
/** Delivery mode (session=origin chat, targets=config targets, both=both). Default: session. */
|
||||
mode?: ExecApprovalForwardingMode;
|
||||
/** Only forward approvals for these agent IDs. Omit = all agents. */
|
||||
agentFilter?: string[];
|
||||
/** Only forward approvals matching these session key patterns (substring or regex). */
|
||||
sessionFilter?: string[];
|
||||
/** Explicit delivery targets (used when mode includes targets). */
|
||||
targets?: ExecApprovalForwardTarget[];
|
||||
};
|
||||
|
||||
export type ApprovalsConfig = {
|
||||
exec?: ExecApprovalForwardingConfig;
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AgentBinding, AgentsConfig } from "./types.agents.js";
|
||||
import type { ApprovalsConfig } from "./types.approvals.js";
|
||||
import type { AuthConfig } from "./types.auth.js";
|
||||
import type { DiagnosticsConfig, LoggingConfig, SessionConfig, WebConfig } from "./types.base.js";
|
||||
import type { BrowserConfig } from "./types.browser.js";
|
||||
@@ -84,6 +85,7 @@ export type ClawdbotConfig = {
|
||||
audio?: AudioConfig;
|
||||
messages?: MessagesConfig;
|
||||
commands?: CommandsConfig;
|
||||
approvals?: ApprovalsConfig;
|
||||
session?: SessionConfig;
|
||||
web?: WebConfig;
|
||||
channels?: ChannelsConfig;
|
||||
|
||||
@@ -72,6 +72,17 @@ export type DiscordActionConfig = {
|
||||
channels?: boolean;
|
||||
};
|
||||
|
||||
export type DiscordExecApprovalConfig = {
|
||||
/** Enable exec approval forwarding to Discord DMs. Default: false. */
|
||||
enabled?: boolean;
|
||||
/** Discord user IDs to receive approval prompts. Required if enabled. */
|
||||
approvers?: Array<string | number>;
|
||||
/** Only forward approvals for these agent IDs. Omit = all agents. */
|
||||
agentFilter?: string[];
|
||||
/** Only forward approvals matching these session key patterns (substring or regex). */
|
||||
sessionFilter?: string[];
|
||||
};
|
||||
|
||||
export type DiscordAccountConfig = {
|
||||
/** Optional display name for this account (used in CLI/UI lists). */
|
||||
name?: string;
|
||||
@@ -124,6 +135,8 @@ export type DiscordAccountConfig = {
|
||||
guilds?: Record<string, DiscordGuildEntry>;
|
||||
/** Heartbeat visibility settings for this channel. */
|
||||
heartbeat?: ChannelHeartbeatVisibilityConfig;
|
||||
/** Exec approval forwarding configuration. */
|
||||
execApprovals?: DiscordExecApprovalConfig;
|
||||
};
|
||||
|
||||
export type DiscordConfig = {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
export * from "./types.agent-defaults.js";
|
||||
export * from "./types.agents.js";
|
||||
export * from "./types.approvals.js";
|
||||
export * from "./types.auth.js";
|
||||
export * from "./types.base.js";
|
||||
export * from "./types.browser.js";
|
||||
|
||||
28
src/config/zod-schema.approvals.ts
Normal file
28
src/config/zod-schema.approvals.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const ExecApprovalForwardTargetSchema = z
|
||||
.object({
|
||||
channel: z.string().min(1),
|
||||
to: z.string().min(1),
|
||||
accountId: z.string().optional(),
|
||||
threadId: z.union([z.string(), z.number()]).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const ExecApprovalForwardingSchema = z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
mode: z.union([z.literal("session"), z.literal("targets"), z.literal("both")]).optional(),
|
||||
agentFilter: z.array(z.string()).optional(),
|
||||
sessionFilter: z.array(z.string()).optional(),
|
||||
targets: z.array(ExecApprovalForwardTargetSchema).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const ApprovalsSchema = z
|
||||
.object({
|
||||
exec: ExecApprovalForwardingSchema,
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
@@ -244,6 +244,15 @@ export const DiscordAccountSchema = z
|
||||
dm: DiscordDmSchema.optional(),
|
||||
guilds: z.record(z.string(), DiscordGuildSchema.optional()).optional(),
|
||||
heartbeat: ChannelHeartbeatVisibilitySchema,
|
||||
execApprovals: z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
approvers: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
agentFilter: z.array(z.string()).optional(),
|
||||
sessionFilter: z.array(z.string()).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { ToolsSchema } from "./zod-schema.agent-runtime.js";
|
||||
import { ApprovalsSchema } from "./zod-schema.approvals.js";
|
||||
import { AgentsSchema, AudioSchema, BindingsSchema, BroadcastSchema } from "./zod-schema.agents.js";
|
||||
import { HexColorSchema, ModelsConfigSchema } from "./zod-schema.core.js";
|
||||
import { HookMappingSchema, HooksGmailSchema, InternalHooksSchema } from "./zod-schema.hooks.js";
|
||||
@@ -220,6 +221,7 @@ export const ClawdbotSchema = z
|
||||
.optional(),
|
||||
messages: MessagesSchema,
|
||||
commands: CommandsSchema,
|
||||
approvals: ApprovalsSchema,
|
||||
session: SessionSchema,
|
||||
cron: z
|
||||
.object({
|
||||
|
||||
Reference in New Issue
Block a user