Skip to main content
PraisonAI bots can pin every interactive button (approve, deny, menu choice) to a specific set of users so other participants in the same chat cannot resolve it.
from praisonaiagents import Agent
from praisonai.bots import Bot

agent = Agent(name="Ops", instructions="Run approved changes only", approval=True)
bot = Bot("slack", agent=agent)
The user clicks Approve in a shared channel; only the requester and owners can resolve that callback.
Before PR #2307 (fixes #2299), any chat member could click another user’s approval button and resolve it. Tool approvals in shared chats are now actor-bound by default in the core wrapper, fail-closed.
Tool-approval Allow/Deny buttons ride a distinct interactive route (cmd:/approve <approval_id> <decision>) that bypasses the generic command policy — authorisation is enforced by the backend’s allowed_actors, not the per-command allow-list. Unknown decision values are rejected before hitting the backend. See Telegram Durable Approval.

Quick Start

1

Default — your bot is already protected

If you run a Telegram, Slack, or Discord bot using the built-in _presentation_approval wrapper, approvals are already pinned to the requester plus any configured owner/admins. No extra code required.
from praisonaiagents import Agent
from praisonai.bots import Bot

agent = Agent(
    name="assistant",
    instructions="You are a helpful assistant.",
)

bot = Bot("telegram", agent=agent)
bot.run()
When the bot sends an approval prompt, only the user who triggered the tool call (plus owner_user_id and admin_users you configured) can click Approve or Deny. Other users in the same group chat are silently rejected.
2

Restrict approvals to a specific user

Pass allowed_actors when calling request_approval from a custom bot adapter to bind a single approval to one or more actors:
from praisonai.bots._presentation_approval import PresentationApprovalHandler

handler = PresentationApprovalHandler()

result = await handler.request_approval(
    tool_name="bash",
    arguments={"command": "rm -rf /tmp/cache"},
    allowed_actors=["telegram:12345"],   # only this user may resolve
    channel_send_func=send_func,
    target=chat_id,
)

if result["approved"]:
    print("Approved by the authorized user")
An unauthorized click leaves the approval pending — the legitimate actor can still resolve it.
3

Register a custom interactive button with authorization

Protect any interactive namespace (menus, polls, custom buttons) using an authorize callback:
from praisonaiagents.bots.interactive import InteractiveRegistry, InteractiveAuthorizer

registry = InteractiveRegistry()

OWNER_IDS = {"telegram:99999", "slack:U01234"}

def only_owners(ctx) -> bool:
    return ctx.user_id in OWNER_IDS

async def my_menu_handler(ctx):
    return f"Selected: {ctx.platform_data.get('value')}"

registry.register("menu", my_menu_handler, authorize=only_owners)
If authorize returns False or raises, dispatch() returns False and the handler is never called. Fail-closed by design.

How It Works

Two layers of protection work together: the registry layer (any interactive callback) and the approval-wrapper layer (tool approvals specifically). State of an approval ID: A resolved approval ID is single-use — any second callback for it is a no-op, preventing replay attacks.

Which authorization shape do I need?


API Reference

SymbolSurfaceModule
InteractiveAuthorizertype alias Callable[[InteractiveContext], bool]praisonaiagents.bots.interactive
InteractiveRegistry.register(..., authorize=)registers handler + optional authorizerpraisonaiagents.bots.interactive
request_approval(..., allowed_actors=)binds approval to actor setpraisonai.bots._presentation_approval
handle_approval_command(..., actor=)enforces actor on resolvepraisonai.bots._presentation_approval
is_authorized(approval_id, actor)helper for custom adapterspraisonai.bots._presentation_approval
audit_log (property)copy of resolved-approval audit entriespraisonai.bots._presentation_approval
history_limit (constructor)bounded FIFO for _resolved_ids + _audit_log (default 1000)praisonai.bots._presentation_approval

Audit Log

Every resolution appends an entry to the bounded in-memory audit log. Retrieve it with the audit_log property (returns deep-copied dicts — callers cannot mutate internal state).
FieldTypeDescription
approval_idstrUnique ID for this approval request
tool_namestrName of the tool awaiting approval
actorstr | NoneUser ID of the clicker (platform-prefixed, e.g. telegram:12345)
decisionstrallow, deny, or always
approvedboolWhether the tool was approved
authorizedboolWhether the actor was in the allowed set
timestampfloatUnix timestamp of the resolution
Three categories of entries are written:
Categoryapprovedauthorized
Authorized approvalTrueTrue
Authorized denialFalseTrue
Unauthorized attemptFalseFalse
Timeouts also append an entry (decision "timeout", approved=False, authorized=True if no actor restriction was set).

Common Patterns

Read the audit log after a session:
from praisonai.bots._presentation_approval import PresentationApprovalHandler

handler = PresentationApprovalHandler()

# ... run your bot session ...

for entry in handler.audit_log:
    print(entry["actor"], entry["decision"], entry["authorized"])
Cap memory for short-lived sessions:
handler = PresentationApprovalHandler(history_limit=200)
The oldest entries are evicted once the limit is exceeded (bounded FIFO). Replay protection still holds for the most recent history_limit approvals. Check authorization without resolving:
if handler.is_authorized(approval_id, actor="telegram:12345"):
    print("This user may resolve the approval")

Text-keyword fallback backends

TelegramApproval, SlackApproval, and DiscordApproval are separate from the _presentation_approval wrapper documented above. They poll for text keyword replies (or inline button taps on Telegram) and have their own actor allowlist added in PR #2582. Set allowed_approvers on the constructor (or via env var) to restrict who can resolve an approval from these backends:
from praisonaiagents import Agent
from praisonai.bots import TelegramApproval

agent = Agent(
    name="ops",
    instructions="Run approved changes only.",
    approval=TelegramApproval(
        chat_id="-100999888",
        allowed_approvers=["11111", "22222"],  # only these users may resolve
    ),
)
agent.start("Deploy the release")
SituationEffect
No allowed_approvers configuredAny responder can resolve (legacy — unsafe in shared chats)
Authorised user taps Approve / replies “yes”Approval resolved (approved=True)
Authorised user taps Deny / replies “no”Approval resolved (approved=False)
Unauthorised user taps Telegram inline buttonTelegram shows “You are not authorized to approve this action.” alert; poll continues
Unauthorised user replies with keyword (any channel)Warning logged; reply ignored; poll continues
Unauthorised user’s message from a different Telegram chat with same message_idIgnored (chat-id binding added in PR #2582)
Approval times outApprovalDecision(approved=False, reason="timed out")
With allowed_approvers=None (the default), legacy “any responder” behaviour is preserved for backward compatibility. You must set allowed_approvers explicitly to secure approvals in shared group chats.
The env var equivalents are TELEGRAM_APPROVERS, SLACK_APPROVERS, and DISCORD_APPROVERS (comma-separated user IDs). See the Approval Protocol page for full configuration tables.

Best Practices

The _presentation_approval wrapper does this automatically by passing allowed_actors including the requesting user, owner_user_id, and admin_users. Only override when you have a specific reason.
Authorizers run synchronously inside dispatch(). Any exception counts as a deny — the callback is rejected and an error is logged. Surface real errors in your application layer, not inside the authorizer.
The in-memory log is bounded by history_limit (default 1000). For compliance or long-running bots, push entries to your logging sink (e.g. structured logging, database) before they are evicted.
registry.register("namespace", handler) without authorize= allows any clicker — identical to the pre-PR behaviour. Useful for non-privileged callbacks such as read-only menus or informational polls.

Approval Protocol

Tool approval backends (console, Slack, Telegram, webhook)

Durable Approvals

Persist approvals to SQLite — including the --approval secure backend with actor authorisation and fail-closed start

Interactive Tool Approval

CLI/terminal approval flow and persistence

Interactive Bot Messages

Buttons, dropdowns, and approval prompts on Telegram/Slack/Discord

Bot Command Access Control

Restrict which users can run which bot commands

Text-keyword Backends

Text-keyword backends and their allowed_approvers allowlist