Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 129 additions & 7 deletions docs/best-practices/bot-security.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,6 @@ WhatsApp has the **strongest security defaults** and serves as the reference imp

## Gateway Pairing

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

For production deployments, use **gateway pairing** to authorize channels dynamically:

Expand All @@ -233,8 +230,7 @@ Without `PRAISONAI_GATEWAY_SECRET`, pairing codes will **not persist across rest
### 2. Generate Pairing Code

```python
# Note: This API is conceptual - verify implementation
from praisonaiagents.gateway.pairing import PairingStore
from praisonai.gateway.pairing import PairingStore

store = PairingStore()
code = store.generate_code(channel_type="telegram")
Expand All @@ -254,8 +250,7 @@ The bot will verify the HMAC signature and authorize the channel.
### 4. Check Status

```python
# Check if channel is paired
# Note: Verify this API exists in current SDK
# Check if channel is paired
paired = store.is_paired("@username", "telegram")
print(f"Channel paired: {paired}")

Expand All @@ -264,6 +259,128 @@ for channel in store.list_paired():
print(f"{channel.channel_type}: {channel.channel_id}")
```

### 5. List Pending Requests

List all pending pairing codes waiting for approval:

```python
from praisonai.gateway.pairing import PairingStore

store = PairingStore()

# All pending codes across every channel
for req in store.list_pending():
print(req["code"], req["channel_type"], req["channel_id"], req["age_seconds"])

# Filter by channel
telegram_only = store.list_pending(channel_type="telegram")
```

**Response Schema:**

| Key | Type | Source | Notes |
|-----|------|--------|-------|
| `code` | `str` | canonical | 8-char pairing code |
| `channel_type` | `str` | canonical | e.g. `"telegram"`, `"discord"`, `"slack"`, `"whatsapp"` |
| `channel_id` | `str \| None` | canonical | Bound channel id if code was generated with one |
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The schema indicates that channel_id is a canonical key stored during code generation. However, the current implementation of generate_code in praisonai/gateway/pairing.py (line 113) does not accept a channel_id argument and does not store it in the _pending dictionary. This will cause req["channel_id"] to be missing or incorrect in the response.

| `created_at` | `float` | canonical | Unix timestamp (seconds) when code was generated |
| `channel` | `str` | UI alias | Same value as `channel_type`, kept for UI banner compatibility |
| `user_id` | `str` | UI alias | Currently equals `code` (see note in `approve()` docstring) |
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This note refers to an approve() docstring, but the corresponding method in the PairingStore class is actually named verify_and_pair(). There is no approve() method in the provided source code.

| `user_name` | `str` | UI alias | Formatted as `"User {code}"` |
| `age_seconds` | `int` | UI alias | `int(now - created_at)` |

<Note>
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.
</Note>

### 6. CLI Commands

Use the `praisonai pairing` commands to manage pairings from the command line:

```bash
# List all paired channels
praisonai pairing list

# Approve a pairing code (this is the exact command shown to users)
praisonai pairing approve telegram abc12345

# Revoke a paired channel
praisonai pairing revoke telegram @username

# Clear all paired channels
praisonai pairing clear
```

**Available Commands:**

| Command | Purpose | Required Args |
|---------|---------|---------------|
| `praisonai pairing list` | List all paired channels | — |
| `praisonai pairing approve PLATFORM CODE [CHANNEL_ID]` | Approve an 8-char pairing code | `platform`, `code` |
| `praisonai pairing revoke PLATFORM CHANNEL_ID` | Revoke a paired channel | `platform`, `channel_id` |
| `praisonai pairing clear` | Clear all paired channels | — |

**Platform values:** `telegram`, `discord`, `slack`, `whatsapp`

**Pairing Flow:**

```mermaid
sequenceDiagram
participant User
participant Bot
participant PairingStore
participant CLI

User->>Bot: unknown message (triggers pairing)
Bot->>PairingStore: generate_code(channel_type, channel_id)
PairingStore-->>Bot: abc12345
Bot->>User: Ask owner to run: praisonai pairing approve telegram abc12345
CLI->>PairingStore: approve(telegram, abc12345)
Comment on lines +335 to +338
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There are two discrepancies in this sequence diagram compared to the implementation in praisonai/gateway/pairing.py:

  1. generate_code is shown taking channel_id, but the implementation only accepts channel_type.
  2. The CLI is shown calling approve(), but the method in PairingStore is named verify_and_pair().

PairingStore->>PairingStore: verify_and_pair()
PairingStore-->>CLI: success
User->>Bot: now authorised
Bot-->>User: response

classDef user fill:#8B0000,stroke:#7C90A0,color:#fff
classDef bot fill:#189AB4,stroke:#7C90A0,color:#fff
classDef success fill:#10B981,stroke:#7C90A0,color:#fff

class User user
class Bot,PairingStore,CLI bot
```

### 7. REST API

The gateway exposes REST endpoints for pairing management:

| Method | Path | Body / Query | Response | Auth Required |
|--------|------|--------------|----------|---------------|
| `GET` | `/api/pairing/pending` | — | `list_pending()` schema | ✅ |
| `POST` | `/api/pairing/approve` | `{ "channel": str, "code": str }` | `{ "approved": true, ... }` | ✅ |
| `POST` | `/api/pairing/revoke` | `{ "channel": str, "user_id": str }` | `{ "revoked": true, ... }` | ✅ |
Comment on lines +358 to +360
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The REST API endpoints for pairing (/api/pairing/pending, /api/pairing/approve, /api/pairing/revoke) are documented here but are missing from the routes definition in praisonai/gateway/server.py (lines 484-491). These endpoints will return a 404 error in the current implementation.


**Example Usage:**

```bash
# List pending requests
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:8000/api/pairing/pending

# Approve a code
curl -X POST -H "Authorization: Bearer $TOKEN" \
-d '{"channel":"telegram","code":"abc12345"}' \
http://localhost:8000/api/pairing/approve

# Revoke a channel
curl -X POST -H "Authorization: Bearer $TOKEN" \
-d '{"channel":"telegram","user_id":"@username"}' \
http://localhost:8000/api/pairing/revoke
```

<Note>
All endpoints are **authenticated** and **rate-limited**. Rate limits are applied per client IP with separate buckets for `pairing_pending`, `pairing_approve`, and `pairing_revoke` operations.
</Note>

## Doctor Security Check

<Warning>
Expand Down Expand Up @@ -472,6 +589,11 @@ def is_verified_user(user_id: str) -> bool:
1. Set `PRAISONAI_GATEWAY_SECRET` env var
2. Codes without persistent secret are temporary

**Problem:** `praisonai pairing approve` reports "Invalid or expired code" even though a code was just generated from the UI
**Solution:**
1. Upgrade to the latest `praisonai` version (fix included in 2026-04-22 release)
2. Older builds had a duplicate internal method that stripped the canonical `code` key when the UI pairing banner was loaded

### Allowlist Issues

**Problem:** Bot not responding to allowed users
Expand Down