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
Default — your bot is already protected
If you run a Telegram, Slack, or Discord bot using the built-in When the bot sends an approval prompt, only the user who triggered the tool call (plus
_presentation_approval wrapper, approvals are already pinned to the requester plus any configured owner/admins. No extra code required.owner_user_id and admin_users you configured) can click Approve or Deny. Other users in the same group chat are silently rejected.Restrict approvals to a specific user
Pass An unauthorized click leaves the approval pending — the legitimate actor can still resolve it.
allowed_actors when calling request_approval from a custom bot adapter to bind a single approval to one or more actors: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
| Symbol | Surface | Module |
|---|---|---|
InteractiveAuthorizer | type alias Callable[[InteractiveContext], bool] | praisonaiagents.bots.interactive |
InteractiveRegistry.register(..., authorize=) | registers handler + optional authorizer | praisonaiagents.bots.interactive |
request_approval(..., allowed_actors=) | binds approval to actor set | praisonai.bots._presentation_approval |
handle_approval_command(..., actor=) | enforces actor on resolve | praisonai.bots._presentation_approval |
is_authorized(approval_id, actor) | helper for custom adapters | praisonai.bots._presentation_approval |
audit_log (property) | copy of resolved-approval audit entries | praisonai.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 theaudit_log property (returns deep-copied dicts — callers cannot mutate internal state).
| Field | Type | Description |
|---|---|---|
approval_id | str | Unique ID for this approval request |
tool_name | str | Name of the tool awaiting approval |
actor | str | None | User ID of the clicker (platform-prefixed, e.g. telegram:12345) |
decision | str | allow, deny, or always |
approved | bool | Whether the tool was approved |
authorized | bool | Whether the actor was in the allowed set |
timestamp | float | Unix timestamp of the resolution |
| Category | approved | authorized |
|---|---|---|
| Authorized approval | True | True |
| Authorized denial | False | True |
| Unauthorized attempt | False | False |
"timeout", approved=False, authorized=True if no actor restriction was set).
Common Patterns
Read the audit log after a session:history_limit approvals.
Check authorization without resolving:
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:
| Situation | Effect |
|---|---|
No allowed_approvers configured | Any 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 button | Telegram 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_id | Ignored (chat-id binding added in PR #2582) |
| Approval times out | ApprovalDecision(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.TELEGRAM_APPROVERS, SLACK_APPROVERS, and DISCORD_APPROVERS (comma-separated user IDs). See the Approval Protocol page for full configuration tables.
Best Practices
Pin approvals to the requester in group chats
Pin approvals to the requester in group chats
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.Persist audit_log if you need long-term accountability
Persist audit_log if you need long-term accountability
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.Related
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 startInteractive 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

