> ## 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).

  When the running gateway binds a request channel, a blocked approval turn can also reach any connected gateway client (web dashboard, TUI, custom SDK app) over the WebSocket transport. See [Server→Client Request Channel](/docs/features/gateway-server-request-channel).

  **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).

  **Out-of-band backends** (`HTTPApproval`, `WebhookApproval`) also accept `allowed_approvers=[...]` (or `HTTP_APPROVERS` / `WEBHOOK_APPROVERS` env vars) with the same semantics — added in [PR #5612](https://github.com/MervinPraison/PraisonAI/pull/5612). `HTTPApproval` additionally mints a single-use URL token so only the operator holding the private link can view or decide.
</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

    from typing import Optional

    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, timeout: Optional[float] = None) -> ApprovalDecision:
            return ApprovalDecision(approved=True, approver="webhook")

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

    <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>

    <Note>
      **Sync backends and the `timeout` kwarg (PR [#5357](https://github.com/MervinPraison/PraisonAI/pull/5357)).** `request_approval_sync` now accepts an optional `timeout: Optional[float] = None`:

      * `None` → wait **indefinitely**; `> 0` → an explicit per-call timeout. The agent's approval-timeout config drives this value.
      * The kwarg is **optional** — a legacy backend without it still works; the agent invokes it without `timeout`.
      * **Never mutate `backend._timeout`** in your own code. The core SDK used to do this, which leaked state onto a backend shared across agents.
    </Note>
  </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>

<Note>
  **`request_approval_sync` is deliberately excluded from `@runtime_checkable` (PR [#5357](https://github.com/MervinPraison/PraisonAI/pull/5357)).** Only `request_approval` (async) is part of the checked protocol surface, so an async-only backend still satisfies `isinstance(backend, ApprovalProtocol)`. The optional `timeout` kwarg on `request_approval_sync` is likewise not part of that surface — legacy sync backends without it keep working.
</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 |

### TypeScript imports

The four core backends have TypeScript parity in `praisonai-ts` — import them flat from `praisonai`. See [Approval Backends (TypeScript)](/docs/js/approval#approval-backends-python-parity).

| Backend | Python import | TypeScript import |
| - | - | - |
| `AutoApproveBackend` | `from praisonaiagents.approval.backends import AutoApproveBackend` | `import { AutoApproveBackend } from 'praisonai'` |
| `ConsoleBackend` | `from praisonaiagents.approval import ConsoleBackend` | `import { ConsoleBackend } from 'praisonai'` |
| `AgentApproval` | `from praisonaiagents.approval import AgentApproval` | `import { AgentApproval } from 'praisonai'` |
| `CallbackBackend` | `from praisonaiagents.approval import CallbackBackend` | `import { CallbackBackend } from 'praisonai'` |

Install a TypeScript backend with `setApprovalBackend(backend, manager?)`; the chat/channel backends (`SlackApproval`, `TelegramApproval`, …) are Python/CLI-only.

***

## 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 typing import Optional

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, timeout: Optional[float] = None) -> ApprovalDecision:
        # Sync fallback for non-async tool execution.
        # `timeout` is optional: None waits indefinitely, > 0 is a per-call limit.
        # Never mutate a shared `self._timeout` here — use the passed value.
        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 |
| `allowed_approvers` | `WEBHOOK_APPROVERS` env, else `None` | Iterable of approver IDs (or comma-separated string) permitted to resolve. `None` = any responder (legacy). Set this when the webhook status endpoint is reachable by more than one trusted operator. |
| `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>

<Tip>
  **Shared-endpoint security:** Use `allowed_approvers` to pin who can resolve. Without it, any party that can reach the status endpoint can resolve a gated tool.

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

  agent = Agent(
      name="ops",
      approval=WebhookApproval(
          webhook_url="https://your-app.com/api/approvals",
          allowed_approvers=["alice", "bob"],  # only these may resolve
      ),
  )
  ```

  Or via environment variable (useful for containerised deployments):

  ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  export WEBHOOK_APPROVERS=alice,bob
  ```
</Tip>

<Note>
  With an allowlist configured, a decision whose `approver` is not in the list is **dropped silently** (warning logged) and polling continues until a legitimate approver resolves it or the request times out. There is no public rejection — an unauthorised responder cannot distinguish "pending" from "rejected" from outside.
</Note>

***

## 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 |
| `allowed_approvers` | `HTTP_APPROVERS` env, else `None` | Iterable of approver IDs (or comma-separated string) permitted to resolve. `None` = any visitor (legacy). Set this whenever the dashboard is reachable by more than one trusted operator. |
| `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>
  **Dashboard security:** When `allowed_approvers` is set, each pending approval mints a single-use token embedded only in the private URL (`http://host:port/approve/<id>?token=<token>`). Only a holder of that URL can view or decide the approval; everyone else gets `403`.

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

  agent = Agent(
      name="ops",
      approval=HTTPApproval(
          host="127.0.0.1",
          port=8899,
          allowed_approvers=["alice", "bob"],
      ),
  )
  ```

  Or via environment variable:

  ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  export HTTP_APPROVERS=alice,bob
  ```
</Tip>

<Note>
  With an allowlist configured, both the page `GET` and the decide `POST` require the per-approval token (`?token=` query param or `X-Approval-Token` header). The token is compared with `secrets.compare_digest`. The decision body's `approver` field must additionally be in the allowlist, otherwise the endpoint returns `403` and the approval stays pending. Without an allowlist, no token is minted (legacy "anyone with the URL can decide" behaviour).
</Note>

***

## 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.

***

## Context Isolation

Approvals are scoped to the Python [context](https://docs.python.org/3/library/contextvars.html) they are granted in. An approval granted inside an `asyncio` task, a `copy_context().run(...)` worker, or a thread spawned with `asyncio.to_thread(...)` stays in that worker — it does not appear in the parent task, and it does not appear in sibling workers copied from the same parent.

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

agent = Agent(name="ops", instructions="Run shell commands", tools=["execute_command"])

async def main():
    # Worker A approves `execute_command` for its own context only.
    # Worker B still gets its own approval prompt — the approval does not leak.
    await asyncio.gather(
        asyncio.to_thread(agent.start, "list /tmp"),
        asyncio.to_thread(agent.start, "list /var/log"),
    )

asyncio.run(main())
```

This is the behaviour you want for anything that fans out: a single human approval must not silently unlock every parallel branch.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Parent as Parent context
    participant Worker_A as Worker A (copy_context)
    participant Worker_B as Worker B (copy_context)
    participant Registry

    Parent->>Worker_A: run(agent.start, …)
    Parent->>Worker_B: run(agent.start, …)
    Worker_A->>Registry: mark_approved(execute_command, {"cmd": "ls /tmp"})
    Note over Registry: Copy-on-write — new set stored in Worker A's context only
    Worker_B->>Registry: is_already_approved(execute_command, {"cmd": "ls /var/log"})
    Registry-->>Worker_B: False — Worker A's grant stays in Worker A
```

Fixed in PraisonAI [PR #5757](https://github.com/MervinPraison/PraisonAI/pull/5757). Before #5757, `mark_approved` mutated the set stored in the per-context approval cache in place; because `contextvars.copy_context()` copies the mapping and not the set, an approval granted in a child context appeared in the parent and in every sibling copied from it. Multi-threaded and `asyncio.to_thread` users of `approval=` therefore saw a sibling worker's approval silently unlock their own tool call. The registry now stores a **new** set each time, so approvals stay in the context that granted them.

### Garbage-collected agents are safe

Letting an `Agent` be collected without calling `.close()` is safe on any thread. `Agent.__del__` releases the agent's process-global registry grants (`auto_approve_tool` entries, session-scoped targets) but **never writes the per-context approval cache** — a finalizer can run on any thread, in any context, so touching a ContextVar from there would clobber an unrelated worker's approvals.

* `Agent.close()` / `Agent.aclose()` — called by your code on the context that owned the agent — still evict that context's approval cache for the agent's scope. Interactive behaviour is unchanged.
* `Agent.__del__` — called by the garbage collector on an unspecified thread — leaves every context's cache alone, and reclaims only the process-global grants.

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

# OK on any thread, in any context — finalizer only reclaims
# process-global grants, never writes a ContextVar.
agent = Agent(name="worker", instructions="…", tools=["execute_command"])
agent.start("…")
del agent

# Explicit close() still clears this context's approval cache for the agent's scope.
agent2 = Agent(name="worker", instructions="…", tools=["execute_command"])
try:
    agent2.start("…")
finally:
    agent2.close()
```

Fixed in PraisonAI [PR #5757](https://github.com/MervinPraison/PraisonAI/pull/5757). The pre-fix finalizer rewrote the per-context approval cache from inside the garbage collector, which could wipe an unrelated context's approval cache.

***

## 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>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.