diff --git a/docs.json b/docs.json
index 89a03e6b..d1312489 100644
--- a/docs.json
+++ b/docs.json
@@ -288,6 +288,7 @@
"docs/features/bot-commands",
"docs/features/bot-gateway",
"docs/features/bot-routing",
+ "docs/features/bot-pairing",
"docs/features/botos",
"docs/features/push-notifications"
]
@@ -649,6 +650,8 @@
"pages": [
"docs/features/cli",
"docs/features/async-agent-scheduler",
+ "docs/features/clarify-tool",
+ "docs/features/tool-availability",
"docs/features/hooks",
"docs/features/hook-events",
"docs/features/dynamic-variables",
diff --git a/docs/best-practices/bot-security.mdx b/docs/best-practices/bot-security.mdx
index 55bbad8f..a33d46fe 100644
--- a/docs/best-practices/bot-security.mdx
+++ b/docs/best-practices/bot-security.mdx
@@ -34,12 +34,17 @@ graph LR
```python
from praisonaiagents import Agent
+from praisonaiagents.bots import BotConfig
# Secure bot with allowlist
+config = BotConfig(
+ allowed_users=["@your_username", "123456789"],
+ unknown_user_policy="deny" # Default secure behavior
+)
+
agent = Agent(
instructions="You are a helpful assistant",
- # Note: Security features shown are conceptual
- # Actual implementation may vary
+ # Bot configuration handled by adapter
)
```
@@ -49,12 +54,19 @@ agent = Agent(
```python
from praisonaiagents import Agent
+from praisonaiagents.bots import BotConfig
+
+# Production security setup with pairing
+config = BotConfig(
+ allowed_users=["@admin_user"],
+ unknown_user_policy="pair", # Secure pairing flow
+ auto_approve_tools=True, # For bot environments
+ group_policy="mention_only" # Only respond when mentioned
+)
-# Production security setup
agent = Agent(
instructions="Secure production assistant",
- # Security configuration would go here
- # when implemented in the SDK
+ # Configure with your bot adapter
)
```
@@ -214,11 +226,7 @@ WhatsApp has the **strongest security defaults** and serves as the reference imp
## Gateway Pairing
-
-**Note:** The pairing system described below represents planned functionality. Current SDK implementation may differ. Verify against actual SDK documentation.
-
-
-For production deployments, use **gateway pairing** to authorize channels dynamically:
+For production deployments, use **gateway pairing** to authorize channels dynamically with the shipped pairing system:
### 1. Set Gateway Secret
@@ -226,54 +234,55 @@ For production deployments, use **gateway pairing** to authorize channels dynami
export PRAISONAI_GATEWAY_SECRET="your-secure-secret-key"
```
-
-Without `PRAISONAI_GATEWAY_SECRET`, pairing codes will **not persist across restarts**. Set this in production.
-
+
+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. Generate Pairing Code
+### 2. Enable Pairing Policy
```python
-# Note: This API is conceptual - verify implementation
-from praisonaiagents.gateway.pairing import PairingStore
+from praisonaiagents.bots import BotConfig
+
+config = BotConfig(
+ allowed_users=["@owner"],
+ unknown_user_policy="pair" # Enable pairing for unknown users
+)
-store = PairingStore()
-code = store.generate_code(channel_type="telegram")
-print(f"Pairing code: {code}") # 8-character hex code
+# Unknown users will automatically receive pairing codes when they DM the bot
```
-### 3. Verify in Channel
+### 3. Approve Pairing Requests
-Send the code to your bot in the target channel:
+When unknown users DM your bot, they receive pairing codes. Approve them via CLI:
+```bash
+# User receives: "Your pairing code: ABCD1234"
+# Owner approves:
+praisonai pairing approve telegram ABCD1234 --label "alice"
```
-/pair abc12345
-```
-The bot will verify the HMAC signature and authorize the channel.
+### 4. Manage Pairings
+
+```bash
+# List all paired channels
+praisonai pairing list
-### 4. Check Status
+# Revoke access for specific channel
+praisonai pairing revoke telegram 987654321
-```python
-# Check if channel is paired
-# Note: Verify this API exists in current SDK
-paired = store.is_paired("@username", "telegram")
-print(f"Channel paired: {paired}")
-
-# List all paired channels
-for channel in store.list_paired():
- print(f"{channel.channel_type}: {channel.channel_id}")
+# Clear all pairings
+praisonai pairing clear --confirm
```
-## Doctor Security Check
+
+For detailed pairing documentation, see the [Bot Pairing](/docs/features/bot-pairing) guide.
+
-
-**Note:** The doctor command shown may not be available in current SDK version. Verify implementation status.
-
+## Doctor Security Check
Use the built-in doctor to audit your bot security configuration:
```bash
-# Note: Verify this command exists
praisonai doctor --category bots
```
diff --git a/docs/features/bot-pairing.mdx b/docs/features/bot-pairing.mdx
new file mode 100644
index 00000000..c31c1006
--- /dev/null
+++ b/docs/features/bot-pairing.mdx
@@ -0,0 +1,415 @@
+---
+title: "Bot Pairing"
+sidebarTitle: "Bot Pairing"
+description: "Secure unknown user onboarding with CLI-approved pairing codes"
+icon: "handshake"
+---
+
+Bot pairing lets unknown users self-request access to your bot with secure 8-character codes that you approve from the CLI.
+
+```mermaid
+graph LR
+ subgraph "Pairing Flow"
+ A[👤 Unknown User] --> B[🤖 Bot DM]
+ B --> C[🔐 Generate Code]
+ C --> D[📱 Send Code]
+ D --> E[💻 CLI Approve]
+ E --> F[✅ User Paired]
+ end
+
+ classDef user fill:#8B0000,stroke:#7C90A0,color:#fff
+ classDef bot fill:#189AB4,stroke:#7C90A0,color:#fff
+ classDef code fill:#F59E0B,stroke:#7C90A0,color:#fff
+ classDef cli fill:#6366F1,stroke:#7C90A0,color:#fff
+ classDef success fill:#10B981,stroke:#7C90A0,color:#fff
+
+ class A user
+ class B bot
+ class C,D code
+ class E cli
+ class F success
+```
+
+## Quick Start
+
+
+
+
+```python
+from praisonaiagents import Agent
+from praisonaiagents.bots 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)
+```
+
+
+
+
+
+```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
+
+```mermaid
+sequenceDiagram
+ participant User
+ participant Bot
+ participant Handler
+ participant PairingStore
+ participant Owner
+
+ User->>Bot: DM message
+ Bot->>Handler: Check if user allowed
+ Handler->>PairingStore: is_paired(channel_id)?
+ PairingStore-->>Handler: No
+ Handler->>PairingStore: generate_code(telegram, channel_id)
+ PairingStore-->>Handler: ABCD1234
+ Handler->>Bot: Send pairing instructions
+ Bot->>User: "Code: ABCD1234"
+
+ Note over Owner: Receives notification
+ Owner->>PairingStore: praisonai pairing approve telegram ABCD1234
+ PairingStore->>PairingStore: Mark channel as paired
+
+ User->>Bot: Next DM
+ Bot->>Handler: Check if user allowed
+ Handler->>PairingStore: is_paired(channel_id)?
+ PairingStore-->>Handler: Yes
+ Handler-->>Bot: Allow message
+ Bot->>User: Normal response
+```
+
+---
+
+## Policy Configuration
+
+### Unknown User Policies
+
+```python
+from praisonaiagents.bots 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 `PaisingStore` implementation)
+- **Automatic Cleanup**: Stale rate limit entries are automatically evicted
+
+---
+
+## CLI Commands
+
+### List Commands
+
+```bash
+# 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: bob
+```
+
+### Approve Command
+
+```bash
+# 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/store
+```
+
+### Revoke Access
+
+```bash
+# 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 --confirm
+```
+
+---
+
+## Platform Support
+
+### Current Implementation
+
+| Platform | Status | Handler Wiring | CLI Support |
+|----------|--------|----------------|-------------|
+| **Telegram** | ✅ Shipped | ✅ Complete | ✅ Full |
+| **Discord** | 🔧 Pending | ❌ Not wired | ✅ CLI ready |
+| **Slack** | 🔧 Pending | ❌ Not wired | ✅ CLI ready |
+| **WhatsApp** | 🔧 Pending | ❌ Not wired | ✅ CLI ready |
+
+
+**Platform Implementation Status**: PR #1504 ships the pairing system and CLI with full Telegram support. Other platform adapters need handler wiring to complete the integration.
+
+
+### Telegram Integration
+
+```python
+# Telegram adapter includes UnknownUserHandler wiring
+from praisonai.bots.telegram import TelegramAdapter
+from praisonaiagents.bots 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
+
+### Code Generation
+
+- **8-character codes**: Hex format (e.g., `ABCD1234`)
+- **HMAC signatures**: Codes are cryptographically signed
+- **Per-install secret**: Auto-generated if `PRAISONAI_GATEWAY_SECRET` unset
+- **Channel binding**: Codes can be bound to specific channel IDs
+
+### Secret Management
+
+```bash
+# 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 /.gateway_secret with 0600 permissions
+# Persists across restarts, unique per installation
+```
+
+
+**Secret Persistence**: Without `PRAISONAI_GATEWAY_SECRET`, a per-install secret is auto-generated and stored at `/.gateway_secret` with mode `0600`. This file is critical for code verification across restarts.
+
+
+### Security Features
+
+```mermaid
+graph TD
+ A[Code Request] --> B{Rate Limited?}
+ B -->|Yes| C[Drop Request ❌]
+ B -->|No| D[Generate HMAC Code]
+ D --> E[Store with TTL]
+ E --> F[Send to User]
+ F --> G[Owner Approval]
+ G --> H{Valid Code?}
+ H -->|Yes| I[Mark Paired ✅]
+ H -->|No| J[Reject ❌]
+
+ classDef security fill:#8B0000,stroke:#7C90A0,color:#fff
+ classDef process fill:#189AB4,stroke:#7C90A0,color:#fff
+ classDef success fill:#10B981,stroke:#7C90A0,color:#fff
+
+ class A,B,D,G,H security
+ class E,F process
+ class I success
+ class C,J security
+```
+
+1. **Rate Limiting**: 600s (10 min) window per channel prevents spam
+2. **HMAC Verification**: Codes are cryptographically signed and verified
+3. **TTL Expiration**: Codes automatically expire after configured time
+4. **Atomic Operations**: Pairing state persisted atomically to disk
+
+---
+
+## User Interaction Flow
+
+### Step-by-Step Process
+
+1. **Unknown User DMs Bot**
+ ```
+ Unknown User: Hello!
+ ```
+
+2. **Bot Generates Pairing Code**
+ ```
+ Bot: Your pairing code: `ABCD1234`
+ Owner: `praisonai pairing approve telegram ABCD1234`
+ ```
+
+3. **Owner Approves via CLI**
+ ```bash
+ $ praisonai pairing approve telegram ABCD1234 --label "alice"
+ ✅ Successfully paired telegram channel 987654321
+ Label: alice
+ ```
+
+4. **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
+
+```bash
+# 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/path
+```
+
+### Environment Variables
+
+```bash
+# 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
+
+
+
+Set `PRAISONAI_GATEWAY_SECRET` explicitly in production environments to ensure consistent code verification across deployments.
+
+```bash
+# Generate secure secret
+openssl rand -hex 32 > gateway_secret.txt
+
+# Set in production
+export PRAISONAI_GATEWAY_SECRET=$(cat gateway_secret.txt)
+```
+
+
+
+Watch for rate limiting warnings in logs - they indicate potential pairing spam or legitimate users hitting limits.
+
+```bash
+# Look for these log patterns:
+# DEBUG: Rate limited channel 123456 (last code: 45.2s ago)
+# INFO: Generated pairing code for user123 on telegram: ABCD1234
+```
+
+
+
+Add labels when approving pairings to identify channels later.
+
+```bash
+# 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 ABCD1234
+```
+
+
+
+Periodically review paired channels and revoke access for inactive users.
+
+```bash
+# Review all pairings
+praisonai pairing list
+
+# Revoke specific channels
+praisonai pairing revoke telegram 987654321
+
+# Clear all if starting fresh
+praisonai pairing clear --confirm
+```
+
+
+
+---
+
+## Related
+
+
+
+Comprehensive bot security and DM policies
+
+
+
+Bot platform setup and configuration
+
+
\ No newline at end of file
diff --git a/docs/features/clarify-tool.mdx b/docs/features/clarify-tool.mdx
new file mode 100644
index 00000000..efecb3eb
--- /dev/null
+++ b/docs/features/clarify-tool.mdx
@@ -0,0 +1,327 @@
+---
+title: "Clarify Tool"
+sidebarTitle: "Clarify Tool"
+description: "Ask focused clarifying questions when agents need user input to proceed"
+icon: "messages-question"
+---
+
+Clarify enables agents to pause mid-task and ask focused questions instead of guessing, improving decision accuracy and user control.
+
+```mermaid
+graph LR
+ subgraph "Clarify Flow"
+ A[🤖 Agent Task] --> B[❓ Clarify Question]
+ B --> C[📱 User Reply]
+ C --> D[✅ Continue Task]
+ end
+
+ classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
+ classDef question fill:#F59E0B,stroke:#7C90A0,color:#fff
+ classDef input fill:#189AB4,stroke:#7C90A0,color:#fff
+ classDef continue fill:#10B981,stroke:#7C90A0,color:#fff
+
+ class A agent
+ class B question
+ class C input
+ class D continue
+```
+
+## Quick Start
+
+
+
+
+```python
+from praisonaiagents import Agent
+from praisonaiagents.tools.clarify import clarify
+
+agent = Agent(
+ name="Writer",
+ instructions="Write code. If requirements are ambiguous, ask clarifying questions.",
+ tools=[clarify],
+)
+
+agent.start("Build me a web scraper")
+# Agent may call: clarify(question="Which language?", choices=["python", "rust"])
+```
+
+
+
+
+
+```python
+from praisonaiagents import Agent
+from praisonaiagents.tools.clarify import clarify, create_cli_clarify_handler
+
+# Setup CLI handler for interactive questions
+handler = create_cli_clarify_handler()
+
+agent = Agent(
+ name="Researcher",
+ instructions="Research topics. Ask for clarification when needed.",
+ tools=[clarify],
+ ctx={"clarify_handler": handler}
+)
+```
+
+
+
+
+---
+
+## How It Works
+
+```mermaid
+sequenceDiagram
+ participant Agent
+ participant Clarify
+ participant Channel
+ participant User
+
+ Agent->>Clarify: clarify(question, choices)
+ Clarify->>Channel: Show question + options
+ Channel->>User: Display prompt
+ User->>Channel: Provide answer
+ Channel->>Clarify: Return response
+ Clarify->>Agent: Continue with answer
+```
+
+| Component | Purpose | Behavior |
+|-----------|---------|----------|
+| **ClarifyTool** | Core tool implementation | Pauses execution for user input |
+| **ClarifyHandler** | Channel-specific behavior | Routes questions to CLI/bot/UI |
+| **Context Integration** | Runtime handler resolution | Uses ctx['clarify_handler'] if available |
+
+---
+
+## Channel Integration
+
+### CLI Usage
+
+```python
+from praisonaiagents.tools.clarify import create_cli_clarify_handler
+
+handler = create_cli_clarify_handler()
+# Shows: 🤔 Which language?
+# Choices:
+# 1. python
+# 2. rust
+# Your choice (number or text): 1
+# Returns: "python"
+```
+
+### Bot Usage
+
+```python
+from praisonaiagents.tools.clarify import create_bot_clarify_handler
+
+async def send_message(channel_id, text):
+ # Send to Telegram/Discord/Slack
+ pass
+
+async def wait_for_reply():
+ # Wait for user response
+ return "python"
+
+handler = create_bot_clarify_handler(send_message, wait_for_reply)
+```
+
+### Custom Context Handler
+
+```python
+from praisonaiagents import Agent
+from praisonaiagents.tools.clarify import clarify
+
+async def my_clarify_handler(question, choices):
+ # Custom UI/web interface
+ return await show_dialog(question, choices)
+
+agent = Agent(
+ name="Assistant",
+ tools=[clarify],
+ ctx={"clarify_handler": my_clarify_handler}
+)
+```
+
+---
+
+## Handler Resolution
+
+The tool uses this priority order for handling questions:
+
+```mermaid
+graph TD
+ A[Clarify Called] --> B{ctx['clarify_handler']?}
+ B -->|Yes| C[Use Context Handler]
+ B -->|No| D{tool.handler exists?}
+ D -->|Yes| E[Use Tool Handler]
+ D -->|No| F[Fallback Message]
+
+ C --> G[Execute Handler]
+ E --> G
+ F --> H["No interactive channel available"]
+ G --> I[Return Response]
+
+ classDef process fill:#189AB4,stroke:#7C90A0,color:#fff
+ classDef fallback fill:#F59E0B,stroke:#7C90A0,color:#fff
+ classDef result fill:#10B981,stroke:#7C90A0,color:#fff
+
+ class A,B,D,G process
+ class F,H fallback
+ class C,E,I result
+```
+
+1. **Context Handler**: `kwargs["ctx"]["clarify_handler"]` (highest priority)
+2. **Tool Handler**: `self.handler` (default `ClarifyHandler()`)
+3. **Fallback**: Returns guidance message when no interactive channel available
+
+---
+
+## Configuration Options
+
+### Tool Schema
+
+```python
+# LLM sees this tool signature:
+{
+ "name": "clarify",
+ "description": "Ask the user a focused clarifying question when genuinely ambiguous. Use sparingly - only when you cannot proceed without their input.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "question": {
+ "type": "string",
+ "description": "The clarifying question to ask"
+ },
+ "choices": {
+ "type": "array",
+ "items": {"type": "string"},
+ "description": "Optional list of predefined answer choices"
+ }
+ },
+ "required": ["question"]
+ }
+}
+```
+
+### Bot Auto-Approval
+
+```python
+from praisonaiagents.bots import BotConfig
+
+config = BotConfig(
+ default_tools=["clarify"], # Included by default
+ auto_approve_tools=True # No approval prompt for clarify
+)
+```
+
+The `clarify` tool is included in the bot's default auto-approve list, so it won't require manual approval in bot environments.
+
+---
+
+## Common Patterns
+
+### Progressive Clarification
+
+```python
+from praisonaiagents import Agent
+from praisonaiagents.tools.clarify import clarify
+
+agent = Agent(
+ name="CodeWriter",
+ instructions="""
+ Write code based on user requests. Use clarify for:
+ 1. Language/framework choice when unspecified
+ 2. Feature priorities when scope is broad
+ 3. Architecture decisions when requirements are complex
+ """,
+ tools=[clarify]
+)
+
+# Example interaction:
+# User: "Build a REST API"
+# Agent: clarify("Which language?", ["python", "node.js", "go"])
+# User: "python"
+# Agent: clarify("Which framework?", ["fastapi", "flask", "django"])
+```
+
+### Context-Aware Questions
+
+```python
+async def smart_clarify_handler(question, choices):
+ """Handler that considers conversation context"""
+ # Check previous messages for hints
+ context = get_conversation_context()
+
+ if "python" in context and "web" in question.lower():
+ # Auto-suggest based on context
+ return "fastapi"
+
+ return await show_user_dialog(question, choices)
+```
+
+### Fallback Behavior
+
+```python
+# When no interactive channel available:
+result = await clarify("Which database?", ["postgres", "mysql"])
+# Returns: "No interactive channel available. Please proceed with your best judgment for: Which database?"
+
+# Agent can handle this gracefully:
+if "no interactive channel" in result.lower():
+ # Use reasonable defaults
+ database = "postgres" # Pick sensible default
+```
+
+---
+
+## Best Practices
+
+
+
+Only call clarify when you genuinely cannot proceed without user input. Don't ask for preferences that have reasonable defaults.
+
+**Good**: "Which API endpoint format?" when building an API
+**Bad**: "Should I use descriptive variable names?" (obvious default)
+
+
+
+When possible, offer specific choices rather than open-ended questions.
+
+**Good**: `choices=["fastapi", "flask", "django"]`
+**Bad**: `"What Python web framework should I use?"` (no choices)
+
+
+
+Always check if the response indicates no interactive channel and proceed with sensible defaults.
+
+```python
+response = await clarify("Pick a color", ["blue", "red"])
+if "no interactive channel" in response.lower():
+ color = "blue" # Use default
+else:
+ color = response
+```
+
+
+
+Frame questions with enough context for users to make informed decisions.
+
+**Good**: "Which authentication method for your user API?"
+**Bad**: "Which auth?" (unclear context)
+
+
+
+---
+
+## Related
+
+
+
+Core tool system and custom tools
+
+
+
+Agent setup and tool integration
+
+
\ No newline at end of file
diff --git a/docs/features/tool-availability.mdx b/docs/features/tool-availability.mdx
new file mode 100644
index 00000000..6da1823f
--- /dev/null
+++ b/docs/features/tool-availability.mdx
@@ -0,0 +1,432 @@
+---
+title: "Tool Availability Gating"
+sidebarTitle: "Tool Availability"
+description: "Hide tools from the LLM when environment dependencies are missing"
+icon: "toggle-on"
+---
+
+Tool availability gating filters unavailable tools at schema-build time, preventing the LLM from hallucinating calls to tools that can't run.
+
+```mermaid
+graph LR
+ subgraph "Availability Gating"
+ A[🔧 Tool Registry] --> B[✅ Check Available]
+ B --> C[📋 LLM Schema]
+ B --> D[❌ Hide Unavailable]
+ C --> E[🤖 Agent Uses Tool]
+ end
+
+ classDef registry fill:#189AB4,stroke:#7C90A0,color:#fff
+ classDef check fill:#F59E0B,stroke:#7C90A0,color:#fff
+ classDef available fill:#10B981,stroke:#7C90A0,color:#fff
+ classDef hidden fill:#8B0000,stroke:#7C90A0,color:#fff
+ classDef agent fill:#6366F1,stroke:#7C90A0,color:#fff
+
+ class A registry
+ class B check
+ class C,E available
+ class D hidden
+```
+
+## Quick Start
+
+
+
+
+```python
+import os
+from praisonaiagents.tools import tool
+
+@tool(availability=lambda: (bool(os.getenv("SERP_API_KEY")), "SERP_API_KEY not set"))
+def search_web(query: str) -> str:
+ """Search the web for information."""
+ api_key = os.getenv("SERP_API_KEY")
+ # ... search implementation
+ return f"Search results for: {query}"
+
+from praisonaiagents import Agent
+
+agent = Agent(
+ name="Researcher",
+ instructions="Research topics the user asks about.",
+ tools=[search_web]
+)
+# If SERP_API_KEY missing → tool hidden from LLM
+# If SERP_API_KEY set → tool appears and works normally
+agent.start("Research quantum computing")
+```
+
+
+
+
+
+```python
+from praisonaiagents.tools.base import BaseTool
+
+class DatabaseTool(BaseTool):
+ name = "query_database"
+ description = "Query the application database"
+
+ def __init__(self, connection_string: str = None):
+ super().__init__()
+ self.connection_string = connection_string or os.getenv("DATABASE_URL")
+
+ def check_availability(self) -> tuple[bool, str]:
+ if not self.connection_string:
+ return False, "DATABASE_URL not configured"
+
+ try:
+ # Quick connection test (must be fast, no I/O heavy operations)
+ import psycopg2
+ return True, ""
+ except ImportError:
+ return False, "psycopg2 package not installed"
+
+ def run(self, query: str) -> str:
+ # Implementation here
+ return f"Query result: {query}"
+```
+
+
+
+
+---
+
+## How It Works
+
+```mermaid
+sequenceDiagram
+ participant Agent
+ participant Registry
+ participant Tool
+ participant LLM
+
+ Agent->>Registry: Get available tools
+ Registry->>Tool: check_availability()
+ Tool-->>Registry: (is_available, reason)
+ alt Available
+ Registry->>LLM: Include in schema
+ LLM->>Tool: Call tool
+ else Unavailable
+ Registry-->>Agent: Skip tool (hidden)
+ Note over LLM: Tool not visible to LLM
+ end
+```
+
+| Phase | Behavior | Performance |
+|-------|----------|-------------|
+| **Schema Build** | Availability checks run once | Zero runtime cost |
+| **Tool Execution** | Only available tools included | No availability overhead |
+| **LLM Interaction** | Only sees usable tools | Prevents hallucination |
+
+---
+
+## Implementation Methods
+
+### Function Decorator
+
+```python
+from praisonaiagents.tools import tool
+import os
+
+# Simple environment check
+@tool(availability=lambda: (bool(os.getenv("API_KEY")), "API_KEY missing"))
+def api_tool(query: str) -> str:
+ return f"API result: {query}"
+
+# Complex dependency check
+def check_docker_available():
+ try:
+ import docker
+ client = docker.from_env()
+ client.ping() # Quick ping, not heavy I/O
+ return True, ""
+ except Exception as e:
+ return False, f"Docker unavailable: {e}"
+
+@tool(availability=check_docker_available)
+def docker_command(cmd: str) -> str:
+ """Run Docker commands."""
+ return f"Docker: {cmd}"
+```
+
+### BaseTool Protocol
+
+```python
+from praisonaiagents.tools.base import BaseTool
+from praisonaiagents.tools.protocols import ToolAvailabilityProtocol
+
+class CloudTool(BaseTool, ToolAvailabilityProtocol):
+ name = "cloud_deploy"
+ description = "Deploy to cloud services"
+
+ def check_availability(self) -> tuple[bool, str]:
+ # Check multiple dependencies
+ if not os.getenv("AWS_ACCESS_KEY"):
+ return False, "AWS credentials not configured"
+
+ try:
+ import boto3
+ # Quick credential test (fast operation only)
+ boto3.Session().get_credentials()
+ return True, ""
+ except Exception as e:
+ return False, f"AWS SDK error: {e}"
+
+ def run(self, service: str) -> str:
+ return f"Deployed {service} to cloud"
+```
+
+### Registry Functions
+
+```python
+from praisonaiagents.tools import list_available_tools, get_registry
+
+# Get only available tools
+available_tools = list_available_tools()
+print(f"Available: {len(available_tools)} tools")
+
+# Get all tools (including unavailable)
+all_tools = get_registry().list_tools()
+print(f"Total registered: {len(all_tools)} tools")
+
+# Check specific tool
+web_tool = get_registry().get("search_web")
+if hasattr(web_tool, 'check_availability'):
+ is_available, reason = web_tool.check_availability()
+ print(f"Web tool available: {is_available}")
+ if not is_available:
+ print(f"Reason: {reason}")
+```
+
+---
+
+## Availability Rules
+
+### Behavior Patterns
+
+```mermaid
+graph TD
+ A[Tool Registration] --> B{Has check_availability?}
+ B -->|No| C[Always Available]
+ B -->|Yes| D[Run Check]
+ D --> E{Check Result?}
+ E -->|True| F[Include in Schema]
+ E -->|False| G[Hide from LLM]
+ E -->|Exception| H[Log Warning + Hide]
+
+ classDef default fill:#189AB4,stroke:#7C90A0,color:#fff
+ classDef available fill:#10B981,stroke:#7C90A0,color:#fff
+ classDef unavailable fill:#8B0000,stroke:#7C90A0,color:#fff
+
+ class A,B,D default
+ class C,F available
+ class G,H unavailable
+```
+
+1. **No Check Method**: Tool is always considered available
+2. **Check Returns True**: Tool included in LLM schema
+3. **Check Returns False**: Tool hidden from LLM
+4. **Check Throws Exception**: Tool hidden + warning logged
+
+### Exception Handling
+
+```python
+# If check_availability() raises an exception:
+def broken_availability_check():
+ raise ValueError("Network unreachable")
+
+@tool(availability=broken_availability_check)
+def network_tool(host: str) -> str:
+ return f"Ping {host}"
+
+# Result: Tool is hidden, log shows:
+# WARNING: Tool 'network_tool' unavailable: Availability check failed: Network unreachable
+```
+
+### Plain Function Registry
+
+```python
+from praisonaiagents.tools import register_tool
+
+def simple_function(text: str) -> str:
+ return f"Processed: {text}"
+
+# Plain functions are always available (no protocol support yet)
+register_tool(simple_function)
+```
+
+---
+
+## Configuration Patterns
+
+### Environment-Based Availability
+
+```python
+import os
+from praisonaiagents.tools import tool
+
+def check_environment(required_vars: list[str]):
+ """Factory for environment-based availability checks."""
+ def check():
+ missing = [var for var in required_vars if not os.getenv(var)]
+ if missing:
+ return False, f"Missing environment variables: {', '.join(missing)}"
+ return True, ""
+ return check
+
+@tool(availability=check_environment(["OPENAI_API_KEY", "PINECONE_API_KEY"]))
+def ai_research(topic: str) -> str:
+ """Research topics using AI and vector search."""
+ return f"AI research on: {topic}"
+
+@tool(availability=check_environment(["SLACK_TOKEN"]))
+def notify_team(message: str) -> str:
+ """Send notifications to team Slack."""
+ return f"Notified team: {message}"
+```
+
+### Service Discovery
+
+```python
+from praisonaiagents.tools import tool
+import socket
+
+def check_service_available(host: str, port: int):
+ """Check if a network service is reachable."""
+ def check():
+ try:
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+ s.settimeout(2) # Keep fast
+ s.connect((host, port))
+ return True, ""
+ except Exception as e:
+ return False, f"Service {host}:{port} unreachable: {e}"
+ return check
+
+@tool(availability=check_service_available("localhost", 5432))
+def query_local_db(sql: str) -> str:
+ """Query local PostgreSQL database."""
+ return f"SQL result: {sql}"
+
+@tool(availability=check_service_available("redis-server", 6379))
+def cache_data(key: str, value: str) -> str:
+ """Cache data in Redis."""
+ return f"Cached {key}: {value}"
+```
+
+### Conditional Tool Loading
+
+```python
+from praisonaiagents import Agent
+from praisonaiagents.tools import list_available_tools
+
+# Build agent with only available tools
+available_tools = list_available_tools()
+print(f"Loading agent with {len(available_tools)} available tools")
+
+agent = Agent(
+ name="AdaptiveAgent",
+ instructions="Use whatever tools are available in the current environment.",
+ tools=available_tools
+)
+
+# Agent automatically adapts to environment capabilities
+```
+
+---
+
+## Performance Guidelines
+
+
+
+Availability checks run at schema-build time and must be fast (< 100ms recommended).
+
+**Good**: Environment variable checks, import tests, quick pings
+**Bad**: Full API calls, heavy file operations, long network requests
+
+```python
+# Fast check
+@tool(availability=lambda: (bool(os.getenv("API_KEY")), "API_KEY missing"))
+
+# Slow check (avoid)
+def slow_check():
+ import requests
+ requests.get("https://api.example.com/health", timeout=30) # Too slow!
+ return True, ""
+```
+
+
+
+For checks that might be expensive, consider caching results.
+
+```python
+import time
+from functools import lru_cache
+
+@lru_cache(maxsize=1)
+def cached_docker_check():
+ """Cache Docker availability for 5 minutes."""
+ try:
+ import docker
+ docker.from_env().ping()
+ return True, ""
+ except Exception as e:
+ return False, str(e)
+
+# Refresh cache periodically
+cached_docker_check.cache_clear()
+```
+
+
+
+Check critical dependencies first, avoid unnecessary work.
+
+```python
+def check_ml_stack():
+ # Check imports first (fast)
+ try:
+ import torch
+ import transformers
+ except ImportError as e:
+ return False, f"Missing ML dependencies: {e}"
+
+ # Then check GPU availability (slower)
+ if not torch.cuda.is_available():
+ return False, "CUDA not available"
+
+ return True, ""
+```
+
+
+
+Design tools to degrade gracefully when dependencies are partially available.
+
+```python
+@tool(availability=lambda: (True, "")) # Always available
+def search_content(query: str) -> str:
+ """Search using best available method."""
+
+ # Try premium search first
+ if os.getenv("PREMIUM_SEARCH_KEY"):
+ return premium_search(query)
+
+ # Fall back to basic search
+ return basic_search(query)
+```
+
+
+
+---
+
+## Related
+
+
+
+Core tool system and registration
+
+
+
+Agent setup and tool integration
+
+
\ No newline at end of file