Skip to content

feat(exec-approvals): reaction-based approval β€” πŸ‘ tapback auto-approves, πŸ‘Ž deniesΒ #24639

@gbharg

Description

@gbharg

Summary

When a user reacts with πŸ‘ (thumbs up) to an exec approval-pending message, the system should automatically resolve the approval as allow-always. When the user reacts with πŸ‘Ž (thumbs down), it should resolve as deny.

Motivation

Currently, approving an exec request requires typing /approve <id> allow-always β€” a 3-step process of:

  1. Finding the approval ID in the message
  2. Typing the full command
  3. Waiting for the resolution

A single tapback reaction is much faster and more natural for mobile users (especially iMessage via BlueBubbles).

Technical Approach

How tapbacks arrive (BlueBubbles)

In extensions/bluebubbles/src/monitor-processing.ts, tapbacks are already detected via parseTapbackText() and resolveTapbackContext(). A πŸ‘ reaction on a message produces:

rawBody = "reacted with πŸ‘"
replyToId = <uuid of replied-to message>
replyToBody = <body of the message being reacted to>

Detection logic

When isTapbackMessage === true AND the replied-to message body matches /Approval required \(id ([a-f0-9]{8})\)/, extract the slug and:

  • πŸ‘ (liked) β†’ call exec.approval.resolve with { id: slug, decision: "allow-always" }
  • πŸ‘Ž (disliked) β†’ call exec.approval.resolve with { id: slug, decision: "deny" }
  • Skip dispatching the tapback to the agent (return early, no session event)

Implementation sketch

// In monitor-processing.ts, after resolving tapbackParsed + replyToBody:
if (isTapbackMessage && tapbackParsed && replyToBody) {
  const approvalMatch = replyToBody.match(/Approval required \(id ([a-f0-9]{8,})\)/);
  if (approvalMatch) {
    const approvalSlug = approvalMatch[1];
    const emoji = tapbackParsed.emoji;
    const decision =
      emoji === "πŸ‘" ? "allow-always" :
      emoji === "πŸ‘Ž" ? "deny" :
      null;
    if (decision) {
      await callGateway({
        method: "exec.approval.resolve",
        params: { id: approvalSlug, decision },
        ...
      });
      return; // skip agent dispatch
    }
  }
}

Generalization

The same pattern applies to other channels that support reactions/tapbacks (Telegram reaction events, Discord reaction events). A shared helper in src/infra/exec-approval-reaction.ts could handle channel-agnostic reaction→approval mapping.

Acceptance Criteria

  • πŸ‘ tapback on an approval message β†’ resolved as allow-always, no agent dispatch
  • πŸ‘Ž tapback on an approval message β†’ resolved as deny, no agent dispatch
  • Non-approval tapbacks are unaffected (still dispatched normally)
  • Works for BlueBubbles channel
  • Tests covering the new logic in monitor-processing.test.ts (or similar)
  • Config option (optional): exec.reactions.enabled: boolean to disable if unwanted

Related

  • src/auto-reply/reply/commands-approve.ts β€” text-based /approve command
  • src/agents/bash-tools.exec.ts β€” where approval slugs are generated (createApprovalSlug)
  • src/gateway/server-methods/exec-approval.ts β€” exec.approval.resolve handler

Metadata

Metadata

Assignees

No one assigned

    Labels

    staleMarked as stale due to inactivity

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions