> ## Documentation Index
> Fetch the complete documentation index at: https://praison.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Approval Protocol

> Extensible tool-execution approval system for agents

Agents can require human approval before executing dangerous tools. Set `approval=True` on any agent to auto-approve, or pass a custom backend for console, webhook, Slack, or any approval channel.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent

agent = Agent(name="assistant", instructions="Ask before destructive tools.", approval=True)
agent.start("List files in the current directory.")
```

<Note>
  **Button-based approvals are now available.** On channels that implement the [`SupportsPresentation`](/docs/features/bot-presentations) protocol (Telegram, Slack, Discord), approval prompts can render as inline buttons instead of relying on `yes`/`no` text replies. Use `MessagePresentation.approval(...)` to build the prompt. The text-keyword backends documented on this page continue to work unchanged — they are the recommended fallback for channels that do not yet support presentations. See [Interactive Bot Messages](/docs/features/bot-presentations).

  **Button-based approvals** (via `MessagePresentation.approval(...)`) are actor-bound by default in shared chats. **Text-keyword backends** (`TelegramApproval`, `SlackApproval`, `DiscordApproval`) require an explicit `allowed_approvers=[...]` allowlist (or `TELEGRAM_APPROVERS` / `SLACK_APPROVERS` / `DISCORD_APPROVERS` env var) to lock down who may resolve an approval — added in [PR #2582](https://github.com/MervinPraison/PraisonAI/pull/2582). Without an allowlist, any responder in the group can approve. See [Interactive Callback Authorization](/docs/features/interactive-callback-authorization).
</Note>

> For unified ApprovalSpec configuration (YAML/CLI/Python), see the [Approval documentation](/docs/features/approval). Backend selection uses the same string values in `--approval` and YAML `backend:` fields.

The user enables approval on an agent; each dangerous tool call pauses until the configured backend allows or denies it.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Approval Protocol"
        Agent[Agent] --> A[🔧 Tool Call]
        A --> B{🔒 Required?}
        B -->|No| D[✅ Execute]
        B -->|Yes| C[📋 Backend]
        C -->|Approved| D
        C -->|Denied| E[❌ Blocked]
    end

    classDef agent fill:#8B0000,color:#fff
    classDef tool fill:#189AB4,color:#fff

    class Agent agent
    class A,B,C,D,E tool
```

## Quick Start

<Steps>
  <Step title="Auto-Approve (Bots / Trusted Envs)">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent

    agent = Agent(
        name="bot",
        instructions="You are a helpful assistant.",
        approval=True
    )
    agent.start("List files in current directory")
    ```
  </Step>

  <Step title="Custom Approval Backend">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent
    from praisonaiagents.approval import ApprovalRequest, ApprovalDecision

    class WebhookBackend:
        async def request_approval(self, request: ApprovalRequest) -> ApprovalDecision:
            # POST to your approval service
            return ApprovalDecision(approved=True, approver="webhook")

        def request_approval_sync(self, request: ApprovalRequest) -> ApprovalDecision:
            return ApprovalDecision(approved=True, approver="webhook")

    <Warning>
    **Calling `request_approval_sync()` from inside an async function is no longer supported.** As of PR #1583, both `WebhookApproval.request_approval_sync()` and `HTTPApproval.request_approval_sync()` delegate to `praisonai._async_bridge.run_sync()`, which raises `RuntimeError` when called from a running event loop. Use `await approval.request_approval(request)` from async code; reserve `request_approval_sync` for synchronous entry points (CLI, sync test harnesses).
    </Warning>

    agent = Agent(
        name="bot",
        instructions="You are a helpful assistant.",
        approval=WebhookBackend()
    )
    agent.start("List files in current directory")
    ```
  </Step>

  <Step title="Default (Console Prompt)">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent

    # No approval= param → dangerous tools prompt in terminal
    agent = Agent(
        name="interactive",
        instructions="You are a helpful assistant.",
    )
    agent.start("Delete temporary files")
    ```
  </Step>
</Steps>

***

## How It Works

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Agent
    participant Backend
    participant Registry

    Agent->>Agent: Check self._approval_backend
    alt Agent has approval backend
        Agent->>Backend: request_approval(ApprovalRequest)
        Backend-->>Agent: ApprovalDecision
    else No agent backend (fallback)
        Agent->>Registry: approve_sync(agent_name, tool, args)
        Registry->>Registry: Check: required? env? yaml? already?
        alt Fast path
            Registry-->>Agent: ApprovalDecision(approved=True)
        else Needs backend
            Registry->>Backend: request_approval(ApprovalRequest)
            Backend-->>Registry: ApprovalDecision
            Registry-->>Agent: ApprovalDecision
        end
    end
```

| Step                 | What Happens                                                    |
| -------------------- | --------------------------------------------------------------- |
| **Agent backend**    | `approval=True` or custom backend on the Agent is checked first |
| **Required?**        | Only tools in the dangerous-tools set need approval             |
| **Env check**        | `PRAISONAI_AUTO_APPROVE=true` skips all prompts                 |
| **YAML check**       | Tools listed in YAML `approve` field are auto-approved          |
| **Already approved** | Once approved in a session, no re-prompt                        |
| **Backend**          | ConsoleBackend (default), AutoApproveBackend, or your custom    |

<Note>
  The **Env check** and **YAML check** rows are honoured on **both** the no-backend and attached-backend paths since [PR #4878](https://github.com/MervinPraison/PraisonAI/pull/4878). Before #4878 an agent with a backend attached (the default `ConsoleBackend` and every chat/gateway front-end) skipped these standing grants and prompted anyway. See [Standing grants and attached backends](/docs/features/approval#standing-grants-and-attached-backends).
</Note>

### `ApprovalRequest` fields

The request object a backend receives, describing the tool call awaiting approval.

| Field                  | Type                           | Default    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| ---------------------- | ------------------------------ | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tool_name`            | `str`                          | —          | Name of the tool requesting approval.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `arguments`            | `Dict[str, Any]`               | —          | Arguments the tool will be called with.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `risk_level`           | `str`                          | `"medium"` | Risk classification (`critical` / `high` / `medium` / `low`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `agent_name`           | `Optional[str]`                | `None`     | Name of the agent that triggered the call.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `session_id`           | `Optional[str]`                | `None`     | Session identifier for tracking.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `context`              | `Dict[str, Any]`               | `{}`       | Arbitrary context dict for backend-specific data.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `approval_id`          | `str`                          | auto       | Stable correlation id that survives a process restart.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `authorized_reviewers` | `Optional[List[str]]`          | `None`     | Reviewer identities allowed to resolve this request. Honoured by the gateway backend (see [per-request reviewer custody](/docs/features/gateway-scoped-approvals#per-request-reviewer-custody)); ignored by backends that do not implement reviewer custody. `None` lets any resolver resolve.                                                                                                                                                                                                                                                                                                                                         |
| `liveness`             | `Optional[Callable[[], bool]]` | `None`     | Zero-arg predicate returning `True` while the originating turn is still live. When present, the registry and the direct-backend path drop a resolution that arrives after the turn was stopped or superseded — **fail-closed**: the tool does not run and no `session`/`always` grant is persisted. `None` (the default) preserves the pre-[#4950](https://github.com/MervinPraison/PraisonAI/pull/4950) behaviour: the request is treated as always live. Populated automatically by the SDK from the run's cooperative cancel signal — application code does not set it. See [Live-Authority Binding](#live-authority-binding). |

<Note>
  `approval_id`, `agent_name`, and `session_id` survive a process restart and re-hydrate from a durable [`ApprovalStore`](/docs/features/durable-approvals). `liveness` is a **per-turn predicate** — it is not persisted and is rebuilt when a turn resumes.
</Note>

### `ApprovalDecision` fields

The decision object a backend returns. Denials can carry a `feedback` string to steer the agent's next turn, and a decision may instead be a **deferral** carrying `escalate=True` that hands the call to a human.

| Field           | Type             | Default  | Description                                                                                                                                                                                                                                                                                                                                                                                                                |
| --------------- | ---------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `approved`      | `bool`           | —        | Whether execution is allowed.                                                                                                                                                                                                                                                                                                                                                                                              |
| `reason`        | `str`            | `""`     | Human-readable reason for the decision.                                                                                                                                                                                                                                                                                                                                                                                    |
| `modified_args` | `Dict[str, Any]` | `{}`     | Optional replacement arguments (empty = use originals).                                                                                                                                                                                                                                                                                                                                                                    |
| `approver`      | `Optional[str]`  | `None`   | Who/what approved (user, system, webhook, …).                                                                                                                                                                                                                                                                                                                                                                              |
| `metadata`      | `Dict[str, Any]` | `{}`     | Backend-specific metadata (timestamps, IPs, …).                                                                                                                                                                                                                                                                                                                                                                            |
| `scope`         | `str`            | `"once"` | How long the decision is remembered: `"once"`, `"session"`, or `"always"`.                                                                                                                                                                                                                                                                                                                                                 |
| `scope_pattern` | `Optional[str]`  | `None`   | Reusable target/pattern to persist for `always`.                                                                                                                                                                                                                                                                                                                                                                           |
| `feedback`      | `Optional[str]`  | `None`   | Free-text guidance supplied by the user when denying a tool call (via `[d] deny & redirect` on `ConsoleBackend` or the Rich/Textual reject dialog). When present on a denial, `tool_execution.py` folds it into the denied tool's `error_msg` as course-correction for the next turn. `None` (default) keeps today's plain-deny semantics.                                                                                 |
| `escalate`      | `bool`           | `False`  | When `True`, the decision is a *deferral to a human*, not a final verdict — the autonomous reviewer was uncertain and asks a person to decide instead of failing open or blindly denying. Enforced invariant: `escalate=True` always forces `approved=False` (fail-closed), so a consumer that ignores this flag keeps today's safe deny-by-default; a human-in-the-loop backend may route escalated requests to a person. |

***

## Configuration Options

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    subgraph "Choose Your Approval Strategy"
        PARAM["🤖 Agent Parameter<br/>Agent(approval=True)"]
        ENV["🌍 Environment Variable<br/>PRAISONAI_AUTO_APPROVE=true"]
        GLOBAL["🔧 Global Registry<br/>set_backend(AutoApproveBackend())"]
        CUSTOM["🔌 Custom Backend<br/>Agent(approval=MyBackend())"]
    end

    classDef param fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef env fill:#10B981,stroke:#7C90A0,color:#fff
    classDef global fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef custom fill:#189AB4,stroke:#7C90A0,color:#fff

    class PARAM param
    class ENV env
    class GLOBAL global
    class CUSTOM custom
```

Since [PR #4878](https://github.com/MervinPraison/PraisonAI/pull/4878), the `PRAISONAI_AUTO_APPROVE` env rung is checked even when a backend is attached — not just for bare-SDK use.

### Agent Parameter (Recommended)

| Value                     | Behavior                                        |
| ------------------------- | ----------------------------------------------- |
| `approval=True`           | Auto-approve all dangerous tools for this agent |
| `approval=False` / `None` | Use registry fallback (default: console prompt) |
| `approval=MyBackend()`    | Use a custom approval backend for this agent    |

### Other Methods

| Method                                                         | Scope        | Use Case                         |
| -------------------------------------------------------------- | ------------ | -------------------------------- |
| `PRAISONAI_AUTO_APPROVE=true`                                  | All agents   | CI/CD, testing                   |
| `get_approval_registry().set_backend(backend)`                 | All agents   | Global policy                    |
| `get_approval_registry().set_backend(backend, agent_name="x")` | Single agent | Registry-level per-agent control |
| YAML `approve: [tool1, tool2]`                                 | Per-task     | Declarative configs              |

***

## Built-in Backends

| Backend                  | Import                                                                      | Behavior                                       |
| ------------------------ | --------------------------------------------------------------------------- | ---------------------------------------------- |
| `AutoApproveBackend`     | `from praisonaiagents.approval.backends import AutoApproveBackend`          | Always approves                                |
| `ConsoleBackend`         | `from praisonaiagents.approval import ConsoleBackend`                       | Rich terminal prompt (default)                 |
| `AgentApproval`          | `from praisonaiagents.approval import AgentApproval`                        | Delegates to another AI agent                  |
| `SlackApproval`          | `from praisonai.bots import SlackApproval`                                  | Slack Block Kit + polling                      |
| `TelegramApproval`       | `from praisonai.bots import TelegramApproval`                               | Telegram inline keyboard + polling             |
| `DiscordApproval`        | `from praisonai.bots import DiscordApproval`                                | Discord embed + text reply polling             |
| `WebhookApproval`        | `from praisonai.bots import WebhookApproval`                                | POST to webhook + poll status                  |
| `HTTPApproval`           | `from praisonai.bots import HTTPApproval`                                   | Local web dashboard                            |
| `GatewayApprovalBackend` | `from praisonai_bot.gateway.gateway_approval import GatewayApprovalBackend` | Queue approvals into the gateway control plane |
| `CallbackBackend`        | `from praisonaiagents.approval import CallbackBackend`                      | Wraps a legacy callback function               |

***

## Gateway-wired approvals

Setting `channels.<platform>.allow_shell: true` with `auto_approve_shell: false` on a gateway channel wires one of these backends automatically — no Python required. If none of them resolve, the gateway installs a `CallbackBackend` that denies shell tools (`execute_command`, `shell_command`, `acp_execute_command`) so shell never silently auto-approves. See [Fail-closed behaviour](/docs/docs/features/bot-shell-execution#fail-closed-behaviour).

The gateway also routes through this same backend precedence when `auto_approve_shell: true` is downgraded on exposed / multi-user surfaces (see [Bot Shell Execution → Exposure-aware auto-approval](/docs/features/bot-shell-execution#exposure-aware-auto-approval)). The precedence itself is unchanged — only the trigger.

The routing precedence, exactly as implemented:

```
approval_mode == "gateway"                → GatewayApprovalBackend()
approval_mode == "http"                   → HTTPApproval(host, port)
approval_mode == "webhook" OR
  approval_webhook_url is set             → WebhookApproval(webhook_url)
                                             ↳ webhook mode with NO url:
                                               warn + fall through to gateway
channel_type == "slack"    + resolved id  → SlackApproval(...)
channel_type == "telegram" + resolved id  → TelegramApproval(...)
channel_type == "discord"  + resolved id  → DiscordApproval(...)
otherwise                                 → GatewayApprovalBackend()  (fallback)
```

<Card title="Bot Shell Execution" icon="terminal" href="/docs/features/bot-shell-execution">
  Full field reference, per-platform ID resolution, env vars, and recipes.
</Card>

<Note>
  **Durable chat backends:** `SlackApproval`, `TelegramApproval`, `DiscordApproval`, `WebhookApproval`, and `HTTPApproval` each accept an optional `store=` parameter. Pass an `ApprovalStore` to persist the pending approval before polling, then call `backend.rehydrate()` on startup to recover in-flight approvals after a restart. The shared `DEFAULT_APPROVAL_TIMEOUT` (300s) from `praisonai_bot.bots._approval_base` is the single source of truth for the wait window. See [Durable Approvals](/docs/docs/features/durable-approvals#durable-chat-native-backends).
</Note>

***

## Custom Backend

Implement `request_approval` (async) and optionally `request_approval_sync` to create any approval channel:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent
from praisonaiagents.approval import ApprovalRequest, ApprovalDecision

class SlackBackend:
    async def request_approval(self, request: ApprovalRequest) -> ApprovalDecision:
        approved = await self._ask_slack(
            f"Approve {request.tool_name} for {request.agent_name}?"
        )
        return ApprovalDecision(
            approved=approved,
            approver="slack",
            metadata={"channel": "#approvals"},
        )

    def request_approval_sync(self, request: ApprovalRequest) -> ApprovalDecision:
        # Sync fallback for non-async tool execution
        return ApprovalDecision(approved=True, approver="slack")

agent = Agent(
    name="slack-bot",
    instructions="You are a helpful assistant.",
    approval=SlackBackend()
)
```

A custom backend may also return `ApprovalDecision(approved=False, escalate=True)` to signal a deferral to a human. Consumers that only check `approved` still fail-closed correctly, since `escalate=True` forces `approved=False`.

***

## Slack Approval

Route tool approvals to Slack — get a rich message with tool details and reply **yes** or **no** to approve or deny.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Agent
    participant SlackApproval
    participant Slack

    Agent->>SlackApproval: request_approval(tool, args)
    SlackApproval->>Slack: Post Block Kit message
    Note over Slack: 🔒 Tool Approval Required<br/>Tool: execute_command<br/>Reply yes/no
    Slack-->>SlackApproval: User replies "yes"
    SlackApproval->>Slack: Update message ✅ Approved
    SlackApproval-->>Agent: ApprovalDecision(approved=True)
```

<Steps>
  <Step title="Install">
    ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    pip install praisonai
    ```
  </Step>

  <Step title="Set Token">
    ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    export SLACK_BOT_TOKEN=xoxb-your-token
    ```
  </Step>

  <Step title="Use It">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent
    from praisonai.bots import SlackApproval

    agent = Agent(
        name="assistant",
        instructions="You are a helpful assistant.",
        tools=[execute_command],
        approval=SlackApproval(channel="#approvals")
    )
    agent.start("Delete old log files")
    ```
  </Step>
</Steps>

<Tip>
  The Slack bot token needs `chat:write` and `channels:history` (or `im:history` for DMs) scopes.
</Tip>

### Configuration

| Parameter           | Default                            | Description                                                                                                                                                                                                                                                                                    |
| ------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `token`             | `SLACK_BOT_TOKEN` env              | Slack bot token (`xoxb-...`)                                                                                                                                                                                                                                                                   |
| `channel`           | Bot's own DM                       | Channel ID, name, or user ID                                                                                                                                                                                                                                                                   |
| `timeout`           | `300` (5 min)                      | Seconds to wait for response                                                                                                                                                                                                                                                                   |
| `poll_interval`     | `3.0`                              | Seconds between polls                                                                                                                                                                                                                                                                          |
| `allowed_approvers` | `SLACK_APPROVERS` env, else `None` | Iterable of Slack user IDs (or comma-separated string) permitted to approve/deny. `None` = any responder (legacy). Set this for shared channels.                                                                                                                                               |
| `store`             | `None`                             | Optional durable `ApprovalStore`. When set, the pending approval is persisted before polling and the decision recorded, so an outstanding approval survives a restart. Call `backend.rehydrate()` on startup to recover in-flight approvals. `None` keeps the legacy in-memory-only behaviour. |

Make Slack approvals restart-safe by adding `store=`:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonai.bots import SlackApproval, ApprovalStore

slack = SlackApproval(
    channel="#approvals",
    store=ApprovalStore(path="~/.praisonai/state/approvals.sqlite"),
)
```

### Approval Keywords

| Action      | Keywords                                                   |
| ----------- | ---------------------------------------------------------- |
| **Approve** | yes, y, approve, approved, ok, allow, go, proceed, confirm |
| **Deny**    | no, n, deny, denied, reject, block, stop, cancel, refuse   |

<Accordion title="Cross-Platform: Telegram Bot → Slack Approval">
  You can use **any** bot platform for user interaction while routing approvals to Slack:

  ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  from praisonaiagents import Agent
  from praisonai.bots import Bot, SlackApproval

  agent = Agent(
      name="assistant",
      instructions="You are a helpful assistant.",
      approval=SlackApproval(channel="U0ABPEV1HK8")  # DM to admin
  )

  # Users talk via Telegram; dangerous tools approved in Slack
  bot = Bot("telegram", agent=agent)
  bot.run()
  ```
</Accordion>

***

## Telegram Approval

Route tool approvals to Telegram — get a message with inline keyboard buttons.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Agent
    participant TelegramApproval
    participant Telegram

    Agent->>TelegramApproval: request_approval(tool, args)
    TelegramApproval->>Telegram: sendMessage + InlineKeyboard
    Note over Telegram: 🔒 Tool Approval Required<br/>✅ Approve  ❌ Deny
    Telegram-->>TelegramApproval: callback_query "approve"
    TelegramApproval->>Telegram: editMessageText ✅ Approved
    TelegramApproval-->>Agent: ApprovalDecision(approved=True)
```

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent
from praisonai.bots import TelegramApproval

agent = Agent(
    name="assistant",
    tools=[execute_command],
    approval=TelegramApproval(chat_id="YOUR_CHAT_ID")  # token from TELEGRAM_BOT_TOKEN env
)
```

<Tip>
  **Group chat security:** Use `allowed_approvers` to restrict who can tap the Approve/Deny buttons. Without it, any group member can resolve the approval.

  ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  agent = Agent(
      name="ops",
      instructions="Run destructive commands only after approval.",
      approval=TelegramApproval(
          chat_id="-100999888",
          allowed_approvers=["999", "111"],  # only these Telegram user IDs may resolve
      ),
  )
  ```

  Or via environment variable (useful for containerised deployments):

  ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  export TELEGRAM_APPROVERS=999,111
  ```
</Tip>

| Parameter           | Default                               | Description                                                                                                                                                                                 |
| ------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `token`             | `TELEGRAM_BOT_TOKEN` env              | Telegram bot token                                                                                                                                                                          |
| `chat_id`           | *(required)*                          | Chat ID to send approvals to                                                                                                                                                                |
| `timeout`           | `300` (5 min)                         | Seconds to wait for response                                                                                                                                                                |
| `poll_interval`     | `2.0`                                 | Seconds between polls                                                                                                                                                                       |
| `allowed_approvers` | `TELEGRAM_APPROVERS` env, else `None` | Iterable of Telegram user IDs (or comma-separated string) permitted to tap Approve/Deny or reply with keywords. `None` = any responder (legacy). Set this for group chats.                  |
| `store`             | `None`                                | Optional durable `ApprovalStore`. When set, the pending approval survives a restart; call `backend.rehydrate()` on startup to recover it. `None` keeps the legacy in-memory-only behaviour. |

<Note>
  Since [PR #2582](https://github.com/MervinPraison/PraisonAI/pull/2582), callback taps are bound to both `message_id` **and** `chat_id`, so a same-`message_id` button in a different chat cannot resolve this approval. An unauthorised tap shows the presser a *"You are not authorized to approve this action."* alert.
</Note>

***

## Discord Approval

Route tool approvals to Discord — get a rich embed and reply **yes** or **no**.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent
from praisonai.bots import DiscordApproval

agent = Agent(
    name="assistant",
    tools=[execute_command],
    approval=DiscordApproval(channel_id="YOUR_CHANNEL_ID")  # token from DISCORD_BOT_TOKEN env
)
```

| Parameter           | Default                              | Description                                                                                                                                                                                 |
| ------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `token`             | `DISCORD_BOT_TOKEN` env              | Discord bot token                                                                                                                                                                           |
| `channel_id`        | *(required)*                         | Channel ID to send approvals to                                                                                                                                                             |
| `timeout`           | `300` (5 min)                        | Seconds to wait for response                                                                                                                                                                |
| `poll_interval`     | `3.0`                                | Seconds between polls                                                                                                                                                                       |
| `allowed_approvers` | `DISCORD_APPROVERS` env, else `None` | Iterable of Discord user IDs (or comma-separated string) permitted to reply Approve/Deny. `None` = any responder (legacy). Set this for shared channels.                                    |
| `store`             | `None`                               | Optional durable `ApprovalStore`. When set, the pending approval survives a restart; call `backend.rehydrate()` on startup to recover it. `None` keeps the legacy in-memory-only behaviour. |

***

## Webhook Approval

POST approval requests to any HTTP endpoint and poll for decisions. Ideal for enterprise dashboards, CI/CD, and custom integrations.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Agent
    participant WebhookApproval
    participant Server

    Agent->>WebhookApproval: request_approval(tool, args)
    WebhookApproval->>Server: POST /approve {tool, args, risk}
    Server-->>WebhookApproval: {status: "pending"}
    loop Poll
        WebhookApproval->>Server: GET /approve/{request_id}
        Server-->>WebhookApproval: {approved: true}
    end
    WebhookApproval-->>Agent: ApprovalDecision(approved=True)
```

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import os
from praisonaiagents import Agent
from praisonai.bots import WebhookApproval

agent = Agent(
    name="assistant",
    tools=[execute_command],
    approval=WebhookApproval(
        webhook_url="https://your-app.com/api/approvals",
        headers={"Authorization": f"Bearer {os.environ['APPROVAL_WEBHOOK_TOKEN']}"},
    ),
)
```

| Parameter       | Default                    | Description                                                                                                                                                                                 |
| --------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `webhook_url`   | `APPROVAL_WEBHOOK_URL` env | URL to POST requests to                                                                                                                                                                     |
| `status_url`    | `webhook_url/{request_id}` | URL template for polling                                                                                                                                                                    |
| `headers`       | `{}`                       | Extra HTTP headers (e.g. auth)                                                                                                                                                              |
| `timeout`       | `300` (5 min)              | Seconds to wait                                                                                                                                                                             |
| `poll_interval` | `5.0`                      | Seconds between polls                                                                                                                                                                       |
| `store`         | `None`                     | Optional durable `ApprovalStore`. When set, the pending approval survives a restart; call `backend.rehydrate()` on startup to recover it. `None` keeps the legacy in-memory-only behaviour. |

<Tip>
  If the webhook returns `{"approved": true}` in the POST response, the decision is immediate — no polling needed.
</Tip>

***

## Agent Approval

Delegate approval decisions to another AI agent. The reviewer sees the tool name and arguments as untrusted data and returns one of `APPROVE`, `DENY`, or `ESCALATE`.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent
from praisonaiagents.approval import AgentApproval

# Security reviewer agent
reviewer = Agent(
    name="security-reviewer",
    instructions="Only approve low-risk read operations. Deny anything destructive.",
)

worker = Agent(
    name="assistant",
    tools=[execute_command],
    approval=AgentApproval(approver_agent=reviewer),
)
```

The reviewer prompt is hardened against prompt injection:

* The tool name and arguments are wrapped in an `<arguments>…</arguments>` untrusted-data block; forged `<arguments>`/`</arguments>` tags in the tool name or values are neutralised to `‹arguments›`.
* Command-like argument values (keys in `{command, cmd, commands, script, shell, code, source, program}`) have any trailing `# …` shell comment stripped before the reviewer sees them; other args pass through unchanged.
* The reviewer replies with one of `APPROVE` / `DENY` / `ESCALATE`. Parsing is fail-closed: empty, ambiguous, mixed, or negated ("DO NOT APPROVE") responses → `DENY`; `ESCALATE` yields `ApprovalDecision(approved=False, escalate=True)`.

The sanitiser only cleans the copy shown to the reviewer — the arguments the tool actually receives are unchanged.

| Parameter        | Default       | Description                         |
| ---------------- | ------------- | ----------------------------------- |
| `approver_agent` | Auto-created  | Agent instance to evaluate requests |
| `llm`            | `gpt-4o-mini` | LLM for default approver agent      |

<Note>
  `AgentApproval` lives in the core SDK (`praisonaiagents.approval`) — no extra dependencies needed.
</Note>

<Warning>
  `AgentApproval` uses an LLM to decide. The prompt has been hardened against prompt injection since [PraisonAI PR #4954](https://github.com/MervinPraison/PraisonAI/pull/4954) (untrusted-data delimiters, shell-comment stripping, tri-state verdict), but an LLM reviewer is still weaker than an explicit policy or a human. Pair it with a stricter backend (gateway policy, human approval on high-risk tools) for defence in depth.
</Warning>

### Handling ESCALATE

When the reviewer is uncertain it returns `escalate=True` — route the request to a human instead of guessing.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
decision = await backend.request_approval(request)

if decision.approved:
    run_tool()
elif decision.escalate:
    # Reviewer deferred to a human — route to a HITL channel
    route_to_human(request, decision)
else:
    deny(decision.reason)
```

See [AgentApproval Hardening](/docs/features/approval-agent-hardening) for the full security model.

***

## HTTP Approval

Serve a local web dashboard for approvals. Open the URL in your browser and click Approve or Deny.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent
from praisonai.bots import HTTPApproval

agent = Agent(
    name="assistant",
    tools=[execute_command],
    approval=HTTPApproval(host="127.0.0.1", port=8899),
)
# When a tool needs approval, open http://127.0.0.1:8899/approve/<request_id>
```

| Parameter | Default       | Description                                                                                                                                                                                 |
| --------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `host`    | `127.0.0.1`   | Bind address                                                                                                                                                                                |
| `port`    | `8899`        | Port to listen on                                                                                                                                                                           |
| `timeout` | `300` (5 min) | Seconds to wait for response                                                                                                                                                                |
| `store`   | `None`        | Optional durable `ApprovalStore`. When set, the pending approval survives a restart; call `backend.rehydrate()` on startup to recover it. `None` keeps the legacy in-memory-only behaviour. |

***

## Multi-Agent Example

Different agents can have different approval policies:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent
from praisonaiagents.approval import AgentApproval
from praisonai.bots import SlackApproval, TelegramApproval, HTTPApproval

# Bot agent: auto-approve everything
bot = Agent(name="bot", approval=True)

# Interactive agent: console prompt (default)
interactive = Agent(name="interactive")

# Slack-approved agent
slack_agent = Agent(name="slack-approved", approval=SlackApproval(channel="#approvals"))

# Telegram-approved agent
telegram_agent = Agent(name="tg-approved", approval=TelegramApproval(chat_id="123456"))

# AI-reviewed agent (no human needed)
reviewer = Agent(name="reviewer", instructions="Only approve safe read operations")
ai_agent = Agent(name="ai-reviewed", approval=AgentApproval(approver_agent=reviewer))

# Local dashboard agent
http_agent = Agent(name="dashboard", approval=HTTPApproval(port=8899))
```

***

## Registry (Advanced)

For global or centralized approval control, use the registry directly:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent, AutoApproveBackend
from praisonaiagents.approval import get_approval_registry

registry = get_approval_registry()

# Global auto-approve
registry.set_backend(AutoApproveBackend())

# Per-agent override via registry
registry.set_backend(AutoApproveBackend(), agent_name="trusted-bot")
```

<Note>
  The `approval=` parameter on `Agent` takes priority over the global registry.
  If an agent has `approval=True`, it will auto-approve regardless of registry settings.
</Note>

***

## Dangerous Tools (Default)

These tools require approval by default:

| Tool              | Risk Level |
| ----------------- | ---------- |
| `execute_command` | critical   |
| `kill_process`    | critical   |
| `execute_code`    | critical   |
| `write_file`      | high       |
| `delete_file`     | high       |
| `move_file`       | high       |
| `execute_query`   | high       |
| `crawl`           | medium     |
| `scrape_page`     | medium     |

Add or remove requirements:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.approval import get_approval_registry

registry = get_approval_registry()
registry.add_requirement("my_dangerous_tool", risk_level="high")
registry.remove_requirement("crawl")
```

***

## ApprovalStoreProtocol

Optional persistence layer for pending approvals — orthogonal to approval backends. Implement `persist`, `list_pending`, and `resolve` to survive restarts.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.approval import ApprovalStoreProtocol, ApprovalRequest, ApprovalDecision

class MyStore:
    async def persist(self, approval_id: str, request: ApprovalRequest, *, expires_at: float) -> None: ...
    async def list_pending(self): ...
    async def resolve(self, approval_id: str, decision: ApprovalDecision) -> None: ...
```

The wrapper ships `ApprovalStore` (SQLite) in `praisonai.bots`. See [Durable Approvals](/docs/features/durable-approvals).

<Note>
  All five chat backends (`SlackApproval`, `TelegramApproval`, `DiscordApproval`, `WebhookApproval`, `HTTPApproval`) accept a `store=` and expose `rehydrate()` via the shared `DurableApprovalMixin` in `praisonai_bot.bots._approval_base`. See [Durable Chat-Native Backends](/docs/docs/features/durable-approvals#durable-chat-native-backends).
</Note>

***

## Live-Authority Binding

Since [PraisonAI #4950](https://github.com/MervinPraison/PraisonAI/pull/4950), every approval that flows through the core `ApprovalRegistry` is bound to the **liveness of the originating turn**. If the user hits `/stop` (or a superseding turn takes over) while an approval is parked on a human — a Telegram tap, a Slack reply, a webhook decision, a local dashboard click — the resolution that lands afterwards is dropped **fail-closed** at the resolution boundary:

* The tool does not run.
* No `session` or `always` grant is persisted for the abandoned turn.
* The returned decision is `ApprovalDecision(approved=False, reason="Turn no longer live (stopped or superseded)")`.

This is inherited automatically by **every** backend that goes through the registry — the built-ins listed on this page, `PresentationApprovalBackend`, and any custom third-party backend. It also applies when a backend is attached directly to the `Agent` via `approval=...` (the direct-backend path).

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Start[👤 User asks agent to<br/>delete production data]
    Prompt[🔒 Approval card sent to<br/>Telegram / Slack / Discord]
    Stop[👤 User types /stop<br/>while approval is pending]
    Late[👆 Reviewer taps Allow<br/>3 minutes later]

    Start --> Prompt
    Prompt --> Stop
    Stop --> Late
    Late --> Drop{Core registry:<br/>request.liveness&#40;&#41; ?}
    Drop -->|False — stopped| FailClosed[❌ Fail-closed<br/>Tool does not run<br/>No grant persisted]

    classDef user fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef prompt fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef check fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef bad fill:#EF4444,stroke:#7C90A0,color:#fff

    class Start,Stop,Late user
    class Prompt prompt
    class Drop check
    class FailClosed bad
```

### Backward-compatibility

`ApprovalRequest.liveness` defaults to `None`, and both `approve_sync` / `approve_async` accept `liveness=None`. Existing code that constructs an `ApprovalRequest` by hand and calls the registry keeps the old always-live behaviour. A liveness probe that **raises** is treated as live (fail-open on the *probe*, never on the decision) so a buggy predicate can never wedge the approval path.

### What the registry does

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Agent
    participant Registry
    participant Backend
    participant Human

    Agent->>Registry: approve_async(..., liveness=turn_alive)
    Registry->>Backend: request_approval(ApprovalRequest(liveness=...))
    Note over Human: User types /stop — turn is stopped
    Human-->>Backend: taps Allow
    Backend-->>Registry: ApprovalDecision(approved=True, scope="always")
    Registry->>Registry: _reject_if_stale(request)
    Note over Registry: liveness&#40;&#41; → False → drop fail-closed
    Registry-->>Agent: ApprovalDecision(approved=False,<br/>reason="Turn no longer live...")
    Note over Registry: mark_approved / persist_scoped_decision NOT called
```

The predicate the SDK supplies follows the effective per-turn cancel token — the same authority an explicit `cancel_token=` argument or a `/stop` targets — and falls back to the agent's interrupt controller when no per-turn token is registered. When neither is attached the request stays always-live, preserving today's behaviour.

***

## Best Practices

<AccordionGroup>
  <Accordion title="Use approval=True for bot agents">
    Set `approval=True` directly on agent constructors for unattended bots. This is the simplest, most agent-centric approach.
  </Accordion>

  <Accordion title="Use environment variable for CI/CD">
    Set `PRAISONAI_AUTO_APPROVE=true` in your CI environment to avoid blocking on prompts during automated testing.
  </Accordion>

  <Accordion title="Create custom backends for production">
    Build webhook or messaging backends that route approvals to the right team. The async protocol supports long-running approval flows.
  </Accordion>

  <Accordion title="Use per-agent backends for multi-agent systems">
    Different agents may need different approval policies. Pass different backends to each agent's `approval=` parameter.
  </Accordion>
</AccordionGroup>

***

## Related

<CardGroup cols={2}>
  <Card title="CLI Tool Approval" icon="terminal" href="/docs/cli/tool-approval">
    `--trust`, `--approve-level`, and `--approval` CLI flags
  </Card>

  <Card title="Messaging Bots" icon="robot" href="/docs/features/messaging-bots">
    Telegram, Discord, Slack, WhatsApp bots
  </Card>

  <Card title="Schedule Tools" icon="clock" href="/docs/tools/schedule-tools">
    Agent-centric scheduling tools
  </Card>

  <Card title="Tools" icon="wrench" href="/docs/tools/tools">
    Built-in tools reference
  </Card>
</CardGroup>
