Skip to content

docs: Owner-DM inline-button pairing approval for Telegram/Discord/Slack bots (PraisonAI PR #1518) #225

@MervinPraison

Description

@MervinPraison

Context

Upstream PR MervinPraison/PraisonAI#1518 was merged on 2026-04-22. It ships the owner-DM inline-button pairing approval system for messaging bots (originally tracked as issue MervinPraison/PraisonAI#1511).

When an unknown user DMs a PraisonAI bot (Telegram / Discord / Slack), the system can now:

  • Deliver a pairing code to the bot owner's DM with inline Approve / Deny buttons
  • Approve the user for future conversations with one tap
  • Fall back to a plain-text code + CLI instruction when owner_user_id is not configured
  • Verify every button callback with an HMAC signature so payloads can't be tampered with

This is a user-facing feature with concrete new config fields, a new runtime workflow, and a new CLI fallback — none of which is currently documented. This issue tracks the documentation work.


Decision: update + new content

After reviewing the docs repo, both a content update and a new page are required:

Action Path Reason
UPDATE docs/features/messaging-bots.mdx The canonical BotConfig reference is here. Two new fields (unknown_user_policy semantics expanded + new owner_user_id) need to be added to the options example and the options table.
NEW PAGE docs/features/bot-unknown-user-pairing.mdx No existing page covers the unknown-user workflow, inline buttons, HMAC signing, or CLI fallback. Warrants a dedicated agent-centric feature page.
UPDATE docs/best-practices/bot-security.mdx Currently labels pairing as "planned functionality". That warning must be removed and the section rewritten against the shipped implementation.
UPDATE docs.json Register the new page under the Features → Communication & Messaging group (right after docs/features/bot-routing).

Placement rules compliance (from AGENTS.md §1.8): Place the new page in docs/features/NOT docs/concepts/. Concepts is human-approved only.


SDK Source of Truth (PR #1518)

Read these files before writing any doc content. Paths are relative to the PraisonAI repo root:

File Purpose
src/praisonai-agents/praisonaiagents/bots/config.py BotConfig dataclass with the two new fields
src/praisonai-agents/praisonaiagents/bots/pairing_types.py UnknownUserPolicy, PairingReply, PairingApprovalResult types (new file)
src/praisonai/praisonai/bots/_unknown_user.py UnknownUserHandler, BotContext, BotAdapter protocol (new file)
src/praisonai/praisonai/bots/_pairing_ui.py PairingUIBuilder, PairingCallbackHandler, HMAC signing (new file)
src/praisonai/praisonai/bots/telegram.py Telegram integration + CallbackQueryHandler
src/praisonai/praisonai/bots/discord.py Discord integration + discord.ui.Button
src/praisonai/praisonai/bots/slack.py Slack integration + @app.action("pair_approve") / "pair_deny"
src/praisonai/tests/integration/bots/test_pairing_owner_dm.py Integration tests (useful for verifying docs examples)

Note: The PraisonAIDocs repo mirrors these via the daily update_repos.sh sync. Until the next sync copies them into praisonaiagents/bots/ and praisonai/bots/ at the repo root, verify directly in the PraisonAI repo at the paths above.


Ground-truth: Config Fields (verbatim from SDK)

From praisonaiagents/bots/config.py (after PR #1518):

# Unknown user policy: "deny" (default), "pair", or "allow"
unknown_user_policy: UnknownUserPolicy = "deny"

# Owner user ID for pairing approvals (platform-specific format)
owner_user_id: Optional[str] = None

def __post_init__(self) -> None:
    if self.unknown_user_policy not in {"deny", "pair", "allow"}:
        raise ValueError(
            f"unknown_user_policy must be one of: deny, pair, allow. Got: {self.unknown_user_policy}"
        )

From praisonaiagents/bots/pairing_types.py (new file):

UnknownUserPolicy = Literal["deny", "pair", "allow"]

@dataclass
class PairingReply:
    user_name: str
    user_id: str
    channel: str
    code: str
    message: str = ""

@dataclass
class PairingApprovalResult:
    success: bool
    message: str
    user_id: Optional[str] = None
    channel: Optional[str] = None

Runtime environment variable

PRAISONAI_CALLBACK_SECRET — HMAC secret used to sign/verify inline-button callback payloads. Defined in praisonai/bots/_pairing_ui.py::_get_callback_secret():

  • If set in env, that value is used.
  • If unset, a random per-process secrets.token_hex(32) is generated — callbacks stop working across restarts.
  • Must be set in production.

Behavioural flow (document this verbatim on the new page)

Unknown DM
    → UnknownUserHandler.handle()
        → PairingStore.is_paired(user_id, channel_type)?  YES → allow message through
        → policy == "allow"                               → allow (no persistent pair)
        → policy == "deny"                                → silently drop
        → policy == "pair":
            → PairingStore.generate_code(channel_type)
            → owner_user_id set + adapter?
                → adapter.send_approval_dm(owner, user_name, code, channel, user_id)
                    → Builds platform-specific inline buttons (HMAC-signed callback_data)
                    → Notifies requester: "Your request has been sent to the owner for approval."
                → Owner taps Approve/Deny
                    → callback_data = "pair:{action}:{channel}:{user_id}:{code}:{sig}"
                    → PairingCallbackHandler.parse_and_verify_callback() checks HMAC
                    → action == "approve": PairingStore.verify_and_pair(...) → notify requester "You've been approved!"
                    → action == "deny":    consume code, reply "❌ Denied"
            → else: CLI fallback
                → reply to requester: "Your pairing code: {code}. Ask the owner to run: praisonai pairing approve {channel_type} {code}"

Required doc changes — detail

1. UPDATE docs/features/messaging-bots.mdx

Current file (lines 337–369) documents BotConfig options but is missing the two new fields. Add them to both the example block and the options table.

In the example config block (around line 337–352):

from praisonaiagents import BotConfig

config = BotConfig(
    token="bot-token",
    # ... existing options ...
    group_policy="mention_only",
    auto_approve_tools=False,

    # NEW (PR #1518)
    unknown_user_policy="pair",   # "deny" (default) | "pair" | "allow"
    owner_user_id="123456789",    # Platform-specific owner ID (Telegram numeric, Discord snowflake, Slack U-id)
)

In the options table (around line 354–369), add these two rows immediately after auto_approve_tools:

Option Type Default Description
unknown_user_policy Literal["deny","pair","allow"] "deny" How to handle messages from users not in allowed_users. deny silently drops, pair runs the owner-approval flow, allow lets everyone through.
owner_user_id Optional[str] None Platform-specific ID of the bot owner. Required for inline-button approvals — without it, "pair" policy falls back to a plain-text CLI instruction.

Add a short cross-reference <Note> linking to the new dedicated page:

<Note>
For the full owner-approval workflow (inline buttons on Telegram / Discord / Slack, HMAC signing, CLI fallback), see [Bot Unknown-User Pairing](/docs/features/bot-unknown-user-pairing).
</Note>

2. NEW page docs/features/bot-unknown-user-pairing.mdx

Follow the AGENTS.md template exactly. Required sections:

Frontmatter

---
title: "Bot Unknown-User Pairing"
sidebarTitle: "Unknown-User Pairing"
description: "Owner-DM inline-button approval for unknown users on Telegram, Discord, and Slack bots"
icon: "user-check"
---

Hero mermaid (graph LR, standard colour scheme)

Show: Unknown DM → Bot → Owner DM (inline buttons) → Approve/Deny → User gets access.

Quick Start (<Steps>)

  • Step 1 – Simplest: enable pairing with two fields.

    from praisonaiagents import Agent
    from praisonaiagents.bots import BotConfig
    
    config = BotConfig(
        token="YOUR_BOT_TOKEN",
        unknown_user_policy="pair",
        owner_user_id="123456789",   # your Telegram/Discord/Slack user ID
    )
    
    agent = Agent(
        name="Support",
        instructions="You are a helpful support assistant.",
    )
    # ... wire config to your bot (telegram/discord/slack) as usual
  • Step 2 – Production env: set PRAISONAI_CALLBACK_SECRET.

    export PRAISONAI_CALLBACK_SECRET="$(openssl rand -hex 32)"

    Explain in one sentence that without this, inline-button callbacks stop working across restarts.

How It Works (sequenceDiagram)

sequenceDiagram
    participant User as Unknown User
    participant Bot
    participant Store as PairingStore
    participant Owner

    User->>Bot: DM
    Bot->>Store: is_paired? → No
    Bot->>Store: generate_code()
    Bot->>Owner: DM with Approve / Deny buttons
    Bot-->>User: "Your request has been sent..."
    Owner->>Bot: Taps Approve (HMAC-signed callback)
    Bot->>Store: verify_and_pair()
    Bot-->>User: "You've been approved! Send me a message."
    User->>Bot: Future DMs flow straight through
Loading

Policies table

Policy Behaviour When to use
"deny" (default) Silently drops messages from users not in allowed_users. Closed / internal bots.
"pair" Generates a code and DMs the owner an Approve/Deny button. Falls back to CLI if owner_user_id is unset. Semi-public bots where you want owner control.
"allow" Lets every unknown user through (no persistent pair). Fully public bots (combine with rate limits / approval protocol).

Add a small mermaid decision diagram ("which policy should I choose?") — AGENTS.md §6.1 requires this when a page offers multiple options.

CLI fallback

When owner_user_id is not set, the bot replies to the requester:

Your pairing code: abc12345. Ask the owner to run: praisonai pairing approve telegram abc12345

Document the exact command: praisonai pairing approve <channel_type> <code> where <channel_type>telegram | discord | slack.

Security: HMAC-signed callbacks

Explain in 2–3 sentences (non-developer friendly):

  • Callback data format: pair:{action}:{channel}:{user_id}:{code}:{sig}
  • sig = first 8 hex chars of HMAC-SHA256(PRAISONAI_CALLBACK_SECRET, "pair:{action}:{channel}:{user_id}:{code}")
  • A tampered callback_data fails verification and is silently ignored + logged.

Include a <Warning> that without PRAISONAI_CALLBACK_SECRET set in env, a random per-process secret is used and inline buttons stop working after restart.

Platform-specific UI (one accordion per platform)

For each, show the on-screen rendering in words plus the underlying mechanism from the SDK:

  • TelegramInlineKeyboardMarkup with ✅ Approve / ❌ Deny; callback via CallbackQueryHandler.
  • Discorddiscord.ui.View + success/danger Buttons; handled via button.callback.
  • Slack — Block Kit actions block with primary/danger buttons; handled via @app.action("pair_approve") / @app.action("pair_deny").

Configuration Options section

Link to the canonical table on messaging-bots.mdx (do not duplicate — AGENTS.md §2 "Configuration Options"). Also add Card links to the auto-generated SDK refs if/when they exist.

Common Patterns

  • Semi-public bot with approval gate (unknown_user_policy="pair" + owner_user_id set)
  • Internal bot ("deny" + explicit allowed_users list)
  • Fully-open public bot ("allow" + rate_limit / approval_protocol)

Best Practices (<AccordionGroup>)

  1. Always set PRAISONAI_CALLBACK_SECRET in production.
  2. Use platform-native user IDs for owner_user_id (Telegram numeric, Discord snowflake, Slack Uxxxxx).
  3. Combine "allow" with rate limiting and tool approval.
  4. Treat a denied pairing as final — the code is consumed so it can't be retried.

Related (<CardGroup cols={2}>)

  • docs/features/messaging-bots
  • docs/best-practices/bot-security

3. UPDATE docs/best-practices/bot-security.mdx

The current file (lines 217–265) says:

Note: The pairing system described below represents planned functionality. Current SDK implementation may differ. Verify against actual SDK documentation.

This is now incorrect — the pairing system is shipped.

Required changes:

  • Remove the <Warning> that pairing is "planned".
  • Replace the /pair abc12345-style user-side example with the new owner-DM flow (Approve / Deny buttons) as the primary pattern.
  • Keep the CLI praisonai pairing approve … flow as the documented fallback, not the main flow.
  • Update the "Gateway Pairing" heading and intro to reflect that unknown_user_policy="pair" is now the entry point, with owner_user_id being the key new config.
  • Add PRAISONAI_CALLBACK_SECRET to the "Self-Hoster Security Checklist → Gateway Pairing Active" accordion (in addition to the existing PRAISONAI_GATEWAY_SECRET).

4. UPDATE docs.json

Under the Features → Communication & Messaging group (currently at lines 281–294 of docs.json), add the new page right after bot-routing:

                   "pages": [
                     "docs/features/messaging-bots",
                     "docs/features/email-bot",
                     "docs/features/whatsapp-bot",
                     "docs/features/bot-commands",
                     "docs/features/bot-gateway",
                     "docs/features/bot-routing",
+                    "docs/features/bot-unknown-user-pairing",
                     "docs/features/botos",
                     "docs/features/push-notifications"
                   ]

Validate JSON after editing.


Style reminders (from AGENTS.md)

  • User-focused, not SDK-focused. Top of new page must open with an agent-centric example.
  • Progressive disclosure — simplest unknown_user_policy="pair" + owner_user_id first; HMAC / fallback / CLI deeper in the page.
  • Active voice, one-sentence section intros, no "In this section we will…".
  • Simple imports only: from praisonaiagents.bots import BotConfig — not deep sub-module paths.
  • Mermaid colour scheme: #8B0000 (inputs/user), #189AB4 (bot/process), #10B981 (success/approved), #F59E0B (pending/intermediate), #6366F1 (config). White text, #7C90A0 strokes.
  • Code examples must run unmodified — verify imports against the PraisonAI repo.
  • Do not duplicate the BotConfig options table on the new page. Link to messaging-bots.mdx.
  • Do not touch docs/concepts/ — those are human-approved only.

Acceptance criteria

  • docs/features/messaging-bots.mdx lists unknown_user_policy and owner_user_id in both the example code block and the options table, with a <Note> linking to the new page.
  • docs/features/bot-unknown-user-pairing.mdx exists with: frontmatter, hero mermaid, Quick Start (<Steps>), How It Works sequenceDiagram, Policies table + decision mermaid, CLI fallback, HMAC-signing security note + PRAISONAI_CALLBACK_SECRET warning, per-platform accordion, Common Patterns, Best Practices (<AccordionGroup>), Related (<CardGroup>).
  • docs/best-practices/bot-security.mdx no longer describes pairing as "planned"; examples reflect the owner-DM button flow and add PRAISONAI_CALLBACK_SECRET to the checklist.
  • docs.json registers docs/features/bot-unknown-user-pairing under "Communication & Messaging" and remains valid JSON.
  • Every code example uses from praisonaiagents... imports that actually resolve against PR #1518.
  • No file under docs/concepts/ is modified.

Generated by documentation-review agent based on merged PR MervinPraison/PraisonAI#1518.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingclaudeTrigger Claude Code analysisdocumentationImprovements or additions to documentationenhancementNew feature or requestsecurity

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions