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:
Lucas Czekaj
2026-01-24 12:56:40 -08:00
committed by GitHub
parent fe7436a1f6
commit 483fba41b9
22 changed files with 1511 additions and 14 deletions

View File

@@ -1,4 +1,5 @@
import type { ExecApprovalDecision } from "../../infra/exec-approvals.js";
import type { ExecApprovalForwarder } from "../../infra/exec-approval-forwarder.js";
import type { ExecApprovalManager } from "../exec-approval-manager.js";
import {
ErrorCodes,
@@ -9,7 +10,10 @@ import {
} from "../protocol/index.js";
import type { GatewayRequestHandlers } from "./types.js";
export function createExecApprovalHandlers(manager: ExecApprovalManager): GatewayRequestHandlers {
export function createExecApprovalHandlers(
manager: ExecApprovalManager,
opts?: { forwarder?: ExecApprovalForwarder },
): GatewayRequestHandlers {
return {
"exec.approval.request": async ({ params, respond, context }) => {
if (!validateExecApprovalRequestParams(params)) {
@@ -69,6 +73,16 @@ export function createExecApprovalHandlers(manager: ExecApprovalManager): Gatewa
},
{ dropIfSlow: true },
);
void opts?.forwarder
?.handleRequested({
id: record.id,
request: record.request,
createdAtMs: record.createdAtMs,
expiresAtMs: record.expiresAtMs,
})
.catch((err) => {
context.logGateway?.error?.(`exec approvals: forward request failed: ${String(err)}`);
});
const decision = await decisionPromise;
respond(
true,
@@ -112,6 +126,11 @@ export function createExecApprovalHandlers(manager: ExecApprovalManager): Gatewa
{ id: p.id, decision, resolvedBy, ts: Date.now() },
{ dropIfSlow: true },
);
void opts?.forwarder
?.handleResolved({ id: p.id, decision, resolvedBy, ts: Date.now() })
.catch((err) => {
context.logGateway?.error?.(`exec approvals: forward resolve failed: ${String(err)}`);
});
respond(true, { ok: true }, undefined);
},
};

View File

@@ -43,6 +43,7 @@ import {
import { startGatewayDiscovery } from "./server-discovery-runtime.js";
import { ExecApprovalManager } from "./exec-approval-manager.js";
import { createExecApprovalHandlers } from "./server-methods/exec-approval.js";
import { createExecApprovalForwarder } from "../infra/exec-approval-forwarder.js";
import type { startBrowserControlServerIfEnabled } from "./server-browser.js";
import { createChannelManager } from "./server-channels.js";
import { createAgentEventHandler } from "./server-chat.js";
@@ -396,7 +397,10 @@ export async function startGatewayServer(
void cron.start().catch((err) => logCron.error(`failed to start: ${String(err)}`));
const execApprovalManager = new ExecApprovalManager();
const execApprovalHandlers = createExecApprovalHandlers(execApprovalManager);
const execApprovalForwarder = createExecApprovalForwarder();
const execApprovalHandlers = createExecApprovalHandlers(execApprovalManager, {
forwarder: execApprovalForwarder,
});
const canvasHostServerPort = (canvasHostServer as CanvasHostServer | null)?.port;