Bot Pairing
Secure unknown user onboarding with CLI-approved pairing codes
Bot pairing lets unknown users self-request access to your bot with secure 8-character codes that you approve from the CLI.
from praisonaiagents import Agent, BotConfig
agent = Agent(name="assistant", instructions="You are a helpful assistant.")
config = BotConfig(unknown_user_policy="pair")
agent.start("An unknown user wants access โ generate a pairing code.")The user DMs the bot for the first time; PraisonAI issues an 8-character code you approve from the CLI.
Quick Start
- Enable Pairing Policy
python from praisonaiagents import Agent from praisonaiagents import BotConfig config = BotConfig( allowed_users=["@owner"], unknown_user_policy="pair", # Enable pairing for unknown users ) # Bot setup with Telegram adapter (other platforms pending) # from praisonai.bots.telegram import TelegramAdapter # bot = TelegramAdapter(config)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: assistantSince PR #5111,
praisonai bot startwires these fields too โ so you can drive the pairing flow from a single-botbot.yamlwithout switching topraisonai gateway start:yaml # bot.yaml platform: telegram token: ${TELEGRAM_BOT_TOKEN} unknown_user_policy: pair owner_user_id: 987654321 agent: name: Assistant instructions: Reply to incoming messages. - Approve Pairing Requests
bash # List pending pairing requests praisonai pairing list # Approve a user's pairing code praisonai pairing approve telegram ABCD1234 --label "alice" # View all paired channels praisonai pairing list
How It Works
Policy Configuration
Unknown User Policies
from praisonaiagents import BotConfig
# Policy options
config = BotConfig(
allowed_users=["@owner", "123456789"],
unknown_user_policy="pair" # Choose one below
)| Policy | Behavior | Use Case |
|---|---|---|
"deny" | Silently drop messages (default) | Private bots, testing |
"allow" | Allow all users (overrides allowed_users) | Public bots |
"pair" | Use pairing flow for approval | Controlled access |
Pairing Rate Limiting
The system includes built-in protection against code generation spam:
- Rate Limit: 10 minutes between code generations per channel
- Code TTL: Codes expire after a configurable time (default: check
PaisingStoreimplementation) - Automatic Cleanup: Stale rate limit entries are automatically evicted
CLI Commands
List Commands
# List all paired channels
praisonai pairing list
# Example output:
# Found 3 paired channels:
#
# Platform: telegram
# Channel: @alice_username
# Paired: 2026-04-22 10:30:15
# Label: alice
#
# Platform: telegram
# Channel: 987654321
# Paired: 2026-04-22 11:45:22
# Label: bobApprove Command
# Approve with auto-resolved channel ID (when code is bound)
praisonai pairing approve telegram ABCD1234
# Approve with explicit channel ID
praisonai pairing approve telegram ABCD1234 987654321
# Approve with human-readable label
praisonai pairing approve telegram ABCD1234 --label "alice"
# Use custom store directory
praisonai pairing approve telegram ABCD1234 --store-dir /path/to/storeRevoke Access
# Revoke specific channel
praisonai pairing revoke telegram 987654321
# Clear all pairings (with confirmation)
praisonai pairing clear
# Are you sure you want to clear ALL paired channels? [y/N]: y
# โ
Cleared 3 paired channels
# Clear without confirmation prompt
praisonai pairing clear --confirmWeb-UI / HTTP API Approval
Approve, list, and revoke pairings from a web admin UI via the gateway’s HTTP API.
sequenceDiagram
participant Browser as Browser (Chainlit/Admin UI)
participant API as POST /api/pairing/approve
participant Store as PairingStore
participant Bus as EventBus
participant Bot
Browser->>API: Approve pairing request
API->>Store: approve(channel, code)
Store-->>API: Success
API->>Bus: pairing_approved event
Bus->>Bot: Event notification
Bot-->>Browser: User acknowledged
classDef user fill:#8B0000,stroke:#7C90A0,color:#fff
classDef api fill:#189AB4,stroke:#7C90A0,color:#fff
classDef event fill:#F59E0B,stroke:#7C90A0,color:#fff
classDef success fill:#10B981,stroke:#7C90A0,color:#fff
class Browser user
class API,Store api
class Bus event
class Bot successEndpoints
List Pending
GET /api/pairing/pending
Returns pending pairing requests awaiting approval.
curl -X GET "http://localhost:8000/api/pairing/pending" \
-H "Cookie: session=<admin-session-cookie>"import httpx
resp = httpx.get(
"http://localhost:8000/api/pairing/pending",
cookies={"session": "<admin-session-cookie>"},
)
resp.raise_for_status()
print(resp.json()) # {"pending": [{"channel_type": "ui", "code": "ABCD1234", "channel_id": "user123", "age_seconds": 45}]}Approve Pairing
POST /api/pairing/approve
Approve a pairing code to authorize the channel.
curl -X POST "http://localhost:8000/api/pairing/approve" \
-H "Content-Type: application/json" \
-H "Cookie: session=<admin-session-cookie>" \
-d '{"channel": "ui", "code": "ABCD1234"}'import httpx
# Admin operator approves a pending UI pairing
resp = httpx.post(
"http://localhost:8000/api/pairing/approve",
json={"channel": "ui", "code": "ABCD1234"},
cookies={"session": "<admin-session-cookie>"},
)
resp.raise_for_status()
print(resp.json()) # {"approved": True, "channel": "ui", "code": "ABCD1234"}Revoke Access
POST /api/pairing/revoke
Revoke authorization for a previously paired channel.
curl -X POST "http://localhost:8000/api/pairing/revoke" \
-H "Content-Type: application/json" \
-H "Cookie: session=<admin-session-cookie>" \
-d '{"channel": "ui", "user_id": "user123"}'import httpx
resp = httpx.post(
"http://localhost:8000/api/pairing/revoke",
json={"channel": "ui", "user_id": "user123"},
cookies={"session": "<admin-session-cookie>"},
)
resp.raise_for_status()
print(resp.json())Reacting to Pairing Approvals (EventBus)
Subscribe to the pairing_approved event to run agent logic the moment a channel is approved โ for example, send a welcome DM, log to your CRM, or warm a per-user memory store.
from praisonaiagents import Agent
from praisonaiagents.bus import get_default_bus
welcomer = Agent(
name="Welcomer",
instructions="Write a one-line friendly welcome message for a new user.",
)
def on_pairing_approved(event):
channel = event.data["channel"]
code = event.data["code"]
welcome = welcomer.start(f"Welcome the new {channel} user with code {code}")
print(welcome)
get_default_bus().subscribe(on_pairing_approved, ["pairing_approved"])Event Payload
| Field | Type | Description |
|---|---|---|
event.type | str | Always "pairing_approved" |
event.data["channel"] | str | Channel type (e.g., "telegram", "ui", "slack") |
event.data["code"] | str | The pairing code that was approved |
Platform Support
Current Implementation
| Platform | Status | Handler Wiring | CLI Support |
|---|---|---|---|
| Telegram | โ Shipped | โ Complete | โ Full |
| Web UI (HTTP API) | โ Shipped | โ via /api/pairing/* routes | โ Full |
| Discord | ๐ง Pending | โ Not wired | โ CLI ready |
| Slack | ๐ง Pending | โ Not wired | โ CLI ready |
| ๐ง Pending | โ Not wired | โ CLI ready |
Telegram Integration
# Telegram adapter includes UnknownUserHandler wiring
from praisonai.bots.telegram import TelegramAdapter
from praisonaiagents import BotConfig
config = BotConfig(
token=os.getenv("TELEGRAM_BOT_TOKEN"),
unknown_user_policy="pair"
)
# Handler automatically wired in Telegram adapter
bot = TelegramAdapter(config)Security Model
Store Hardening (PraisonAI #3974)
PraisonAI #3974 hardens the pairing store without changing its public API โ legacy plaintext pairing.json stores still load, so there is nothing to migrate.
At-rest hashing
Since #3974, only a salted SHA-256 of each pending code is written to pairing.json. Reading the file never yields a usable code, and a rainbow table is useless without the per-install HMAC secret used as salt.
{
"pending": [
{
"code_hash": "9f2cโฆ64-hex-charsโฆa1",
"signature": "โฆ",
"channel_type": "telegram",
"channel_id": "tg_42",
"created_at": 1755255600.0
}
]
}On a fresh process, praisonai pairing list (or PairingStore.list_pending()) against a store whose code was created in another process returns entries with code: None and the code_hash โ the pending request stays visible, the raw code does not.
Store-level brute-force lockout
verify_and_pair() counts failed verifications per channel_type and refuses further attempts once the ceiling trips โ uniformly across every approval surface (HTTP route, in-chat Approve/Deny button, CLI praisonai pairing approve). Tune via constructor kwargs:
| Constructor kwarg | Default | Meaning |
|---|---|---|
max_failures | 5 | Consecutive failed verifications before lockout trips |
lockout_window | 60.0 s | Sliding window that counts failures |
lockout_cooldown | 300.0 s | How long a channel_type is refused once tripped |
A success clears the counter; a valid HMAC presented against the wrong channel_id does not (otherwise a repeated wrong-binding guess would reset the budget). Once locked out, even a valid code for that channel_type is refused until the cooldown expires โ the lockout is store-level, not surface-level.
Unambiguous alphabet
Generated codes use ABCDEFGHJKLMNPQRSTUVWXYZ23456789 โ 8 characters, dropping 0/O/1/I so codes read aloud or retyped from another device are not mis-entered. Length is unchanged; entropy rises to log2(32^8) = 40 bits.
Code Generation
- 8-character codes: Unambiguous alphabet
ABCDEFGHJKLMNPQRSTUVWXYZ23456789(drops0/O/1/I, since #3974) - HMAC signatures: Codes are cryptographically signed
- Hashed at rest: Only a salted SHA-256 of each code is persisted (since #3974)
- Per-install secret: Auto-generated if
PRAISONAI_GATEWAY_SECRETunset - Channel binding: Codes can be bound to specific channel IDs
Secret Management
# Option 1: Set explicit gateway secret (recommended for production)
export PRAISONAI_GATEWAY_SECRET="your-secure-secret-key"
# Option 2: Auto-generated per-install secret
# Stored at ~/.praisonai/gateway/.gateway_secret with 0600 permissions
# Persists across restarts, unique per installationAn existing 0o666 file left over from an older install is fixed automatically on the next startup โ no manual chmod needed. See Security โ PR #2850 for the hardening summary.
Security Features
- Rate Limiting: 600s (10 min) window per channel prevents spam
- HMAC Verification: Codes are cryptographically signed and verified
- TTL Expiration: Codes automatically expire after configured time
- Atomic Operations: Pairing state persisted atomically to disk
User Interaction Flow
Step-by-Step Process
- Unknown User DMs Bot
Unknown User: Hello!- Bot Generates Pairing Code
Bot: Your pairing code: `ABCD1234`
Owner: `praisonai pairing approve telegram ABCD1234`- Owner Approves via CLI
$ praisonai pairing approve telegram ABCD1234 --label "alice"
โ
Successfully paired telegram channel 987654321
Label: alice- User Can Now Interact Normally
Unknown User: Hello!
Bot: Hi there! How can I help you today?Rate Limit Handling
If a user tries to generate codes too frequently:
User: Hello!
Bot: [no response - rate limited]
# In logs:
# DEBUG: Rate limited channel 987654321 (last code: 120.5s ago)The user must wait for the rate limit window (10 minutes) to expire before requesting a new code.
Configuration Options
Store Directory
# Default: ~/.praisonai/pairing/
praisonai pairing list
# Custom directory
praisonai pairing list --store-dir /custom/path
# All commands support --store-dir
praisonai pairing approve telegram ABCD1234 --store-dir /custom/pathEnvironment Variables
# Explicit gateway secret (recommended for production)
export PRAISONAI_GATEWAY_SECRET="your-256-bit-secret"
# Custom store directory (optional)
export PRAISONAI_STORE_DIR="/custom/store/path"Best Practices
Production Secret Management
Set PRAISONAI_GATEWAY_SECRET explicitly in production environments to ensure consistent code verification across deployments.
# Generate secure secret
openssl rand -hex 32 > gateway_secret.txt
# Set in production
export PRAISONAI_GATEWAY_SECRET=$(cat gateway_secret.txt)Monitor Rate Limits
Watch for rate limiting warnings in logs – they indicate potential pairing spam or legitimate users hitting limits.
# Look for these log patterns:
# DEBUG: Rate limited channel 123456 (last code: 45.2s ago)
# INFO: Generated pairing code for user123 on telegram: ABCD1234Use Descriptive Labels
Add labels when approving pairings to identify channels later.
# Good - easy to identify
praisonai pairing approve telegram ABCD1234 --label "alice-work"
praisonai pairing approve telegram EFGH5678 --label "bob-personal"
# Less helpful
praisonai pairing approve telegram ABCD1234Regular Pairing Audits
Periodically review paired channels and revoke access for inactive users.
# Review all pairings
praisonai pairing list
# Revoke specific channels
praisonai pairing revoke telegram 987654321
# Clear all if starting fresh
praisonai pairing clear --confirm