Bot Unknown-User Pairing

Owner-DM inline-button approval for unknown users on Telegram, Discord, and Slack bots

Bot pairing enables owner-approval for unknown users with inline Approve/Deny buttons sent directly to your DM.

python
from praisonaiagents import Agent, BotConfig

agent = Agent(name="assistant", instructions="You are a helpful assistant.")
config = BotConfig(unknown_user_policy="pair", owner_user_id="123456789")
agent.start("Unknown user requests access — notify the owner.")

The user messages the bot before they are allowlisted; the owner receives Approve/Deny buttons in DM.

Diagram

Unknown User Pairing Flow

Approve

Deny

👤 Unknown User

🤖 Bot

🔒 Generate Code

📩 Owner DM

✅❌ Decision

🟢 User Paired

🔴 Request Denied

💬 Future Messages

Quick Start

  1. Enable Pairing
    python
    from praisonaiagents import Agent
    from praisonaiagents 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.",
    )

    The same config from gateway.yaml — both keys are read per-channel (PR #2856):

    yaml
    channels:
      telegram:
        token: ${TELEGRAM_BOT_TOKEN}
        unknown_user_policy: pair
        owner_user_id: ${TELEGRAM_OWNER_ID}
        routes:
          default: assistant
  2. Multi-host deployments (optional)

    On a single host, callback signing works out of the box — the signing key is the persisted gateway secret at ~/.praisonai/gateway/.gateway_secret (mode 0600, auto-created on first use), so buttons survive restarts with no operator action.

    Set PRAISONAI_CALLBACK_SECRET to a shared value only when running the gateway on multiple hosts/replicas so each verifies the same signatures:

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

How It Works

Diagram
OwnerPairingStoreBotUnknown UserOwnerPairingStoreBotUnknown UserDMis_paired? → Nogenerate_code()DM with Approve / Deny buttons“Your request has been sent…”Taps Approve (HMAC-signed callback with platform type)verify_and_pair()“You’ve been approved! Send me a message.”Future DMs flow straight through

The pairing system intercepts messages from users not in allowed_users and routes them through an approval workflow controlled by the unknown_user_policy, publishing a pairing_approved event on the default EventBus after every successful approval (CLI, inline button, or web UI).


Policy Options

Diagram

deny

pair

allow

Yes

No

📩 Message from Unknown User

unknown_user_policy?

🔇 Silently drop message

🔗 Generate pairing code

✅ Process message directly

owner_user_id set?

📱 Inline approval buttons

📝 CLI fallback instructions

PolicyBehaviourWhen 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).

Which Policy Should I Choose?

Diagram

Only me/team

Selected people

Anyone

🤖 Setting up a bot?

Who should use it?

Internal Bot

Gated Bot

Public Bot

✅ Use ‘deny’
+ allowed_users list

✅ Use ‘pair’
+ owner_user_id

✅ Use ‘allow’
+ rate limiting


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

The owner can then approve manually:

bash
praisonai pairing approve <channel_type> <code>

Where <channel_type> is one of: telegram, discord, slack.


Security: HMAC-signed Callbacks

All inline-button callbacks are cryptographically signed to prevent tampering:

  • Callback format: pair:{action}:{channel}:{user_id}:{code}:{sig}
  • Signature: First 8 hex chars of HMAC-SHA256(callback_secret, payload),
  • where callback_secret is PRAISONAI_CALLBACK_SECRET when set, otherwise
  • the persisted gateway secret from ~/.praisonai/gateway/.gateway_secret.
  • Verification: Tampered callback_data fails verification and is silently ignored + logged

Platform-specific UI

Telegram

Uses InlineKeyboardMarkup with ✅ Approve / ❌ Deny buttons. Callbacks are handled via CallbackQueryHandler that parses the signed callback_data and verifies the HMAC signature.

What the owner sees:

User @username wants to chat. Approve access?
[✅ Approve] [❌ Deny]

Implementation: Telegram’s InlineKeyboardButton with callback_data containing the signed pairing payload. The {channel} field in the pair:{action}:{channel}:{user_id}:{code}:{sig} format contains the platform identifier (telegram), not the chat ID.

Discord

Uses discord.ui.View with success (green) and danger (red) button styles. Handled via button.callback method that verifies the HMAC signature in the custom_id.

What the owner sees:

User username#1234 wants to chat. Approve access?
[✅ Approve] [❌ Deny]

Implementation: Discord’s Button components in an Action Row with HMAC-signed custom_id values. The {channel} field in the callback format is the platform identifier (discord).

Slack

Uses Block Kit actions block with primary (blue) and danger (red) button styles. Handled via @app.action("pair_approve") and @app.action("pair_deny") decorators that verify the signature in the button’s value.

What the owner sees:

*@username* wants to chat. Approve access?
[✅ Approve] [❌ Deny]

Implementation: Slack Block Kit buttons with HMAC-signed values and dedicated action handlers. The {channel} field in the callback format is the platform identifier (slack).


Configuration Options

For the complete BotConfig options including unknown_user_policy and owner_user_id, see the canonical reference at Messaging Bots Configuration.


Common Patterns

Semi-public Bot

python
from praisonaiagents import Agent
from praisonaiagents import BotConfig

config = BotConfig(
    token="your-bot-token",
    unknown_user_policy="pair",    # Enable approval workflow
    owner_user_id="123456789",     # Your platform user ID
)

agent = Agent(
    name="Support Bot",
    instructions="Help users with their questions",
)

Perfect for customer support or community bots where you want to vet new users.

Internal Bot

python
from praisonaiagents import Agent  
from praisonaiagents import BotConfig

config = BotConfig(
    token="your-bot-token",
    unknown_user_policy="deny",           # Block unknown users
    allowed_users=["user123", "user456"], # Explicit allowlist
)

agent = Agent(
    name="Internal Assistant", 
    instructions="Help with internal tasks",
)

For team or company-internal bots with a fixed user list.

Public Bot

python
from praisonaiagents import Agent
from praisonaiagents import BotConfig

config = BotConfig(
    token="your-bot-token", 
    unknown_user_policy="allow",     # Open to everyone
    auto_approve_tools=False,        # Still require tool approval
)

agent = Agent(
    name="Public Assistant",
    instructions="Help anyone with general questions",
)

For fully public bots. Combine with rate limiting and tool approval for safety.

Gateway YAML

yaml
channels:
  telegram_support:
    token: ${TELEGRAM_BOT_TOKEN}
    allowed_users: ["123456789"]
    unknown_user_policy: "pair"
    owner_user_id: "123456789"

Gateway mode configuration for the pairing workflow.


Best Practices

Share the callback secret only across multiple hosts

On a single host, inline buttons survive restarts by default — the signing key is the persisted gateway secret at ~/.praisonai/gateway/.gateway_secret (mode 0600, auto-created on first use). No environment variable is needed.

Set PRAISONAI_CALLBACK_SECRET to a shared value only when running the gateway on multiple hosts/replicas so each verifies the same signatures:

bash
# Generate and export the same value on every host
export PRAISONAI_CALLBACK_SECRET="$(openssl rand -hex 32)"

# Or set permanently in your deployment
echo "PRAISONAI_CALLBACK_SECRET=$(openssl rand -hex 32)" >> .env
Use platform-native user IDs for owner_user_id

Each platform has its own user ID format:

  • Telegram: Numeric ID (e.g., 123456789)
  • Discord: Snowflake ID (e.g., 123456789012345678)
  • Slack: User ID format (e.g., U1234ABCD)

Find your ID by messaging the bot directly and checking the logs, or use platform-specific methods.

Combine 'allow' policy with rate limiting and tool approval

If using unknown_user_policy="allow" for a public bot, protect yourself with:

python
config = BotConfig(
    unknown_user_policy="allow",
    auto_approve_tools=False,     # Users still need approval for dangerous tools
    debounce_ms=2000,            # Coalesce rapid messages
)

Consider also implementing rate limiting at the platform level.

Treat denied pairings as final

When you deny a pairing request, the code is consumed and cannot be retried. The user must send a new message to generate a fresh code. This prevents spam and ensures each approval decision is deliberate.


Related