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