diff --git a/docs.json b/docs.json
index ce565e51..5dd8683b 100644
--- a/docs.json
+++ b/docs.json
@@ -290,6 +290,7 @@
"docs/features/bot-gateway",
"docs/features/bot-routing",
"docs/features/bot-pairing",
+ "docs/features/bot-unknown-user-pairing",
"docs/features/botos",
"docs/features/push-notifications"
]
diff --git a/docs/best-practices/bot-security.mdx b/docs/best-practices/bot-security.mdx
index 2fcfc479..af81e30a 100644
--- a/docs/best-practices/bot-security.mdx
+++ b/docs/best-practices/bot-security.mdx
@@ -224,21 +224,78 @@ channels:
WhatsApp has the **strongest security defaults** and serves as the reference implementation for other channels.
-## Gateway Pairing
+## Owner-DM Pairing
-For production deployments, use **gateway pairing** to authorize channels dynamically with the shipped pairing system:
+The pairing system is now shipped and enables owner-approval for unknown users with inline Approve/Deny buttons sent directly to your DM.
+
+For production deployments, use **owner-DM pairing** to authorize unknown users dynamically:
+
+### 1. Set Callback Secret
+
+```bash
+export PRAISONAI_CALLBACK_SECRET="$(openssl rand -hex 32)"
+```
+
+
+Without `PRAISONAI_CALLBACK_SECRET`, inline-button callbacks will **not work across restarts**. Set this in production.
+
+
+### 2. Configure Unknown User Policy
+
+```python
+from praisonaiagents import Agent
+from praisonaiagents.bots import BotConfig
+
+config = BotConfig(
+ token="your-bot-token",
+ unknown_user_policy="pair", # Enable owner-approval workflow
+ owner_user_id="123456789", # Your platform user ID
+)
+
+agent = Agent(
+ name="Support Bot",
+ instructions="Help users with their questions",
+)
+```
+
+### 3. Owner Approval Workflow
+
+When an unknown user messages your bot:
+
+1. Bot generates a pairing code
+2. **Owner receives DM with inline Approve/Deny buttons**
+3. Owner clicks Approve → User is permanently approved
+4. Owner clicks Deny → Request is rejected
+
+**CLI Fallback:** If `owner_user_id` is not set, the bot replies:
+```
+Your pairing code: abc12345. Ask the owner to run: praisonai pairing approve telegram abc12345
+```
-### 1. Set Gateway Secret
+### 4. Manual Approval (CLI)
+
+Owners can approve pairing requests manually:
```bash
-export PRAISONAI_GATEWAY_SECRET="your-secure-secret-key"
+# Approve a specific pairing code
+praisonai pairing approve telegram abc12345
+
+# Approve for Discord
+praisonai pairing approve discord def67890
+
+# Approve for Slack
+praisonai pairing approve slack ghi13579
```
+## Gateway Pairing
+
+For production deployments, use **gateway pairing** to authorize channels dynamically with the shipped pairing system:
+
The gateway secret is optional - if unset, a per-install secret is auto-generated at `/.gateway_secret` with `0600` permissions and reused across restarts.
-### 2. Enable Pairing Policy
+### Enable Pairing Policy
```python
from praisonaiagents.bots import BotConfig
@@ -251,7 +308,7 @@ config = BotConfig(
# Unknown users will automatically receive pairing codes when they DM the bot
```
-### 3. Approve Pairing Requests
+### Approve Pairing Requests
When unknown users DM your bot, they receive pairing codes. Approve them via CLI:
@@ -261,7 +318,7 @@ When unknown users DM your bot, they receive pairing codes. Approve them via CLI
praisonai pairing approve telegram ABCD1234 --label "alice"
```
-### 4. Manage Pairings
+### Manage Pairings
```bash
# List all paired channels
@@ -278,7 +335,7 @@ praisonai pairing clear --confirm
For detailed pairing documentation, see the [Bot Pairing](/docs/features/bot-pairing) guide.
-### 5. List Pending Requests
+### List Pending Requests
List all pending pairing codes waiting for approval:
@@ -312,7 +369,7 @@ telegram_only = store.list_pending(channel_type="telegram")
Canonical keys (`code`, `channel_type`, `channel_id`, `created_at`) are the stable contract. The `channel`, `user_id`, `user_name`, and `age_seconds` aliases are provided for UI consumers and should not be relied on for scripting — use the canonical keys.
-### 6. CLI Commands
+### CLI Commands
Use the `praisonai pairing` commands to manage pairings from the command line:
@@ -442,8 +499,10 @@ Remediation: Consider allowlists for DM security and 'mention_only' group policy
- [ ] `PRAISONAI_GATEWAY_SECRET` set
- - [ ] Pairing codes generated and shared securely
- - [ ] All production channels paired and verified
+ - [ ] `PRAISONAI_CALLBACK_SECRET` set (for inline buttons)
+ - [ ] `unknown_user_policy` configured (`deny`/`pair`/`allow`)
+ - [ ] `owner_user_id` set for inline approvals
+ - [ ] Pairing workflow tested and verified
- [ ] Revocation process documented
diff --git a/docs/features/bot-unknown-user-pairing.mdx b/docs/features/bot-unknown-user-pairing.mdx
new file mode 100644
index 00000000..0fef0855
--- /dev/null
+++ b/docs/features/bot-unknown-user-pairing.mdx
@@ -0,0 +1,354 @@
+---
+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"
+---
+
+Bot pairing enables owner-approval for unknown users with inline Approve/Deny buttons sent directly to your DM.
+
+```mermaid
+graph LR
+ subgraph "Unknown User Pairing Flow"
+ A[👤 Unknown User] --> B[🤖 Bot]
+ B --> C[🔒 Generate Code]
+ C --> D[📩 Owner DM]
+ D --> E{✅❌ Decision}
+ E -->|Approve| F[🟢 User Paired]
+ E -->|Deny| G[🔴 Request Denied]
+ F --> H[💬 Future Messages]
+ end
+
+ classDef user fill:#8B0000,stroke:#7C90A0,color:#fff
+ classDef bot fill:#189AB4,stroke:#7C90A0,color:#fff
+ classDef process fill:#F59E0B,stroke:#7C90A0,color:#fff
+ classDef approve fill:#10B981,stroke:#7C90A0,color:#fff
+ classDef deny fill:#EF4444,stroke:#7C90A0,color:#fff
+
+ class A user
+ class B bot
+ class C,D process
+ class E process
+ class F,H approve
+ class G deny
+```
+
+## Quick Start
+
+
+
+```python
+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.",
+)
+```
+
+
+
+```bash
+export PRAISONAI_CALLBACK_SECRET="$(openssl rand -hex 32)"
+```
+Without this, inline-button callbacks stop working across bot restarts.
+
+
+
+---
+
+## How It Works
+
+```mermaid
+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
+```
+
+The pairing system intercepts messages from users not in `allowed_users` and routes them through an approval workflow controlled by the `unknown_user_policy`.
+
+---
+
+## Policy Options
+
+```mermaid
+graph TB
+ MSG["📩 Message from Unknown User"] --> POLICY{"unknown_user_policy?"}
+
+ POLICY -->|"deny"| DENY["🔇 Silently drop message"]
+ POLICY -->|"pair"| PAIR["🔗 Generate pairing code"]
+ POLICY -->|"allow"| ALLOW["✅ Process message directly"]
+
+ PAIR --> OWNER_ID{"owner_user_id set?"}
+ OWNER_ID -->|"Yes"| INLINE["📱 Inline approval buttons"]
+ OWNER_ID -->|"No"| CLI["📝 CLI fallback instructions"]
+
+ classDef input fill:#6366F1,stroke:#7C90A0,color:#fff
+ classDef check fill:#F59E0B,stroke:#7C90A0,color:#fff
+ classDef deny fill:#EF4444,stroke:#7C90A0,color:#fff
+ classDef allow fill:#10B981,stroke:#7C90A0,color:#fff
+ classDef pair fill:#189AB4,stroke:#7C90A0,color:#fff
+
+ class MSG input
+ class POLICY,OWNER_ID check
+ class DENY deny
+ class ALLOW allow
+ class PAIR,INLINE,CLI pair
+```
+
+| 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). |
+
+### Which Policy Should I Choose?
+
+```mermaid
+graph TD
+ START["🤖 Setting up a bot?"] --> USERS{"Who should use it?"}
+
+ USERS -->|"Only me/team"| INTERNAL["Internal Bot"]
+ USERS -->|"Selected people"| GATED["Gated Bot"]
+ USERS -->|"Anyone"| PUBLIC["Public Bot"]
+
+ INTERNAL --> DENY_REC["✅ Use 'deny'
+ allowed_users list"]
+ GATED --> PAIR_REC["✅ Use 'pair'
+ owner_user_id"]
+ PUBLIC --> ALLOW_REC["✅ Use 'allow'
+ rate limiting"]
+
+ classDef start fill:#6366F1,stroke:#7C90A0,color:#fff
+ classDef question fill:#F59E0B,stroke:#7C90A0,color:#fff
+ classDef option fill:#189AB4,stroke:#7C90A0,color:#fff
+ classDef recommendation fill:#10B981,stroke:#7C90A0,color:#fff
+
+ class START start
+ class USERS question
+ class INTERNAL,GATED,PUBLIC option
+ class DENY_REC,PAIR_REC,ALLOW_REC recommendation
+```
+
+---
+
+## 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
+```
+
+Where `` 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(PRAISONAI_CALLBACK_SECRET, payload)`
+- **Verification**: Tampered `callback_data` fails verification and is silently ignored + logged
+
+
+Without `PRAISONAI_CALLBACK_SECRET` set in your environment, a random per-process secret is used and inline buttons stop working after bot restart. Always set this in production:
+
+```bash
+export PRAISONAI_CALLBACK_SECRET="$(openssl rand -hex 32)"
+```
+
+
+---
+
+## Platform-specific UI
+
+
+
+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.
+
+
+
+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.
+
+
+
+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.
+
+
+
+---
+
+## Configuration Options
+
+For the complete `BotConfig` options including `unknown_user_policy` and `owner_user_id`, see the canonical reference at [Messaging Bots Configuration](/docs/features/messaging-bots#configuration-options).
+
+---
+
+## Common Patterns
+
+
+
+```python
+from praisonaiagents import Agent
+from praisonaiagents.bots 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.
+
+
+
+```python
+from praisonaiagents import Agent
+from praisonaiagents.bots 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.
+
+
+
+```python
+from praisonaiagents import Agent
+from praisonaiagents.bots 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.
+
+
+
+---
+
+## Best Practices
+
+
+
+Generate a strong secret and set it as an environment variable:
+
+```bash
+# Generate and export
+export PRAISONAI_CALLBACK_SECRET="$(openssl rand -hex 32)"
+
+# Or set permanently in your deployment
+echo "PRAISONAI_CALLBACK_SECRET=$(openssl rand -hex 32)" >> .env
+```
+
+Without this, inline buttons stop working when your bot restarts.
+
+
+
+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.
+
+
+
+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.
+
+
+
+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
+
+
+
+ Complete bot configuration and setup
+
+
+ Security best practices for bots
+
+
\ No newline at end of file
diff --git a/docs/features/messaging-bots.mdx b/docs/features/messaging-bots.mdx
index cb1b0232..0721151b 100644
--- a/docs/features/messaging-bots.mdx
+++ b/docs/features/messaging-bots.mdx
@@ -424,6 +424,8 @@ config = BotConfig(
thread_threshold=500, # Auto-thread if response > N chars (0 = disabled)
group_policy="mention_only", # Group chat policy: respond_all, mention_only, command_only
auto_approve_tools=True, # Auto-approve safe tool executions (default: True for chat bots)
+ unknown_user_policy="pair", # "deny" (default) | "pair" | "allow"
+ owner_user_id="123456789", # Platform-specific owner ID (Telegram numeric, Discord snowflake, Slack U-id)
)
```
@@ -443,6 +445,8 @@ config = BotConfig(
| `group_policy` | `str` | `"mention_only"` | Group chat behavior: `respond_all`, `mention_only`, or `command_only` |
| `default_tools` | `list[str]` | `["search_web", "web_crawl", "schedule_add", "schedule_list", "schedule_remove", "store_memory", "search_memory", "store_learning", "search_learning"]` | Safe tools auto-injected when the agent has no tools configured. Destructive tools (`execute_command`, `delete_file`, `write_file`, `shell_command`) are excluded from auto-injection even if listed. |
| `auto_approve_tools` | `bool` | `True` | Skip confirmation for safe tool execution. Chat bots can't show CLI approval prompts, so this defaults to `True`. Set `False` to require approval (only useful if you wire a chat-level approval flow). |
+| `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. |
| `retry_attempts` | `int` | `3` | Number of retry attempts for failed operations |
| `polling_interval` | `float` | `1.0` | Interval for polling mode (seconds) |
@@ -460,6 +464,10 @@ config = BotConfig(
- `command_only` — Bot only responds to `/commands`
+
+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).
+
+
---
## CLI Capabilities