Skip to main content
--approval secure is a hardened approval backend that persists decisions across restarts, authorises approvers against an allowlist, and binds each decision to an unguessable approval ID — not to a message ID that anyone could race.
For the high-level approval lifecycle model, see the Approval concept page.
from praisonaiagents import Agent

agent = Agent(
    name="assistant",
    instructions="Ask before destructive tools.",
    approval="secure",
)
agent.start("Remove stale cache files.")
The user triggers a risky tool; the secure backend stores the request until an authorised actor approves or denies via the channel callback.

How It Works

A risky tool call is stored durably until an authorised actor resolves it by unguessable approval ID.

Telegram: zero-config wiring

When the transport is a Telegram bot, Agent(approval="secure") (alias "presentation") is auto-wired on TelegramBot.start() — the bot supplies the channel renderer and target, and calls rehydrate() to restore pending approvals. No register_approval_backend() call is required. See Telegram Durable Approval.

Quick Start

1

Enable via CLI

Run the gateway with the secure approval backend:
export PRAISONAI_APPROVAL_ACTORS="user123,admin456"

praisonai run --approval secure path/to/agent.yaml
The alias --approval presentation is equivalent.
2

Set Allowed Approvers

PRAISONAI_APPROVAL_ACTORS is required and must be non-empty when using --approval secure. The gateway fails closed if this variable is not set:
export PRAISONAI_APPROVAL_ACTORS="user123,user456,admin789"
Values are comma-separated actor (user) IDs from your chat platform.
3

Programmatic Usage

from praisonaiagents import Agent
from praisonai.bots import PresentationApprovalBackend, ApprovalStore

store = ApprovalStore(path="~/.praisonai/state/approvals.sqlite")

backend = PresentationApprovalBackend(
    store=store,
    allowed_actors={"user123", "admin456"},
    timeout=300.0,
)

agent = Agent(
    name="Secure Agent",
    instructions="Execute tasks with approval.",
    approval=backend,
)

agent.start("Deploy the new version")
4

Rehydrate on Restart

After a restart, rehydrate pending approvals so late decisions still resolve:
backend = PresentationApprovalBackend(
    store=ApprovalStore(path="~/.praisonai/state/approvals.sqlite"),
    allowed_actors={"admin456"},
)

rehydrated = await backend.rehydrate()
print(f"Restored {rehydrated} pending approvals")

Security Properties

The secure backend fixes four gaps in the per-channel backends (TelegramApproval, SlackApproval, etc.):
PropertyDefault backends--approval secure
Actor authorisation❌ Anyone can approvePRAISONAI_APPROVAL_ACTORS allowlist
Pending stateIn-memory (lost on restart)✅ SQLite persistence
Decision bindingBy message ID✅ By unguessable approval ID
LLM classifier in decision path✅ (some)❌ Removed
Unknown decision valuesSilently treated as deny✅ Rejected at the Telegram callback boundary (fail-closed)

Unguessable Approval ID

Each approval request carries a cryptographically unguessable approval_id in the button callback payload. A deny or approve decision binds to that specific ID — not to the Telegram/Slack message ID, which an attacker could observe or guess. Replays are rejected: once an ID is resolved it cannot be re-used.

Persistence and Rehydration

Pending approvals are written to ~/.praisonai/state/approvals.sqlite (honouring PRAISONAI_HOME). On restart, rehydrate() restores all pending approvals with the original allowed_actors re-applied — a restored approval cannot become unrestricted just because the process restarted.

CLI Flags

FlagDescription
--approval secureEnable PresentationApprovalBackend
--approval presentationAlias for --approval secure

Environment Variables

VariableRequiredDescription
PRAISONAI_APPROVAL_ACTORSYes — must be non-emptyComma-separated user IDs allowed to approve tool calls
PRAISONAI_HOMENoOverride base directory; approval store defaults to $PRAISONAI_HOME/state/approvals.sqlite
PRAISONAI_APPROVAL_ACTORS must be set and non-empty. The gateway fails closed (refuses to run) if this variable is missing or empty when --approval secure is active.

Storage

Approvals persist to SQLite at:
~/.praisonai/state/approvals.sqlite
Override with PRAISONAI_HOME:
export PRAISONAI_HOME=/var/lib/praisonai
# → /var/lib/praisonai/state/approvals.sqlite

API Reference

from praisonai.bots import PresentationApprovalBackend, ApprovalStore
MethodDescription
rehydrate()Restore pending approvals from the store; returns count
handle_callback(approval_id, decision, actor)Resolve a button tap; returns True when handled, False when unknown/replay/unauthorised
request_approval(request)ApprovalProtocol implementation — wait for a decision

Best Practices

The backend is fail-closed: without an allowlist it cannot authorise any actor and will reject all approvals. Set this variable in your deployment environment before starting the gateway.
In a long-lived gateway, call await backend.rehydrate() after construction so any approval that was pending when the process last stopped can still be resolved. Without rehydration, pending approvals are invisible.
Passing a store=ApprovalStore(...) makes approvals durable across restarts. Without a store, the backend uses in-memory state only and pending approvals are lost on restart.
Actor IDs are platform-specific strings (Telegram user IDs, Slack user IDs, etc.). If you run bots on multiple platforms, ensure the IDs in PRAISONAI_APPROVAL_ACTORS match the platform your approvers use.

Approval (Concept)

High-level approval lifecycle model

Durable Approvals

Durable approval storage

Message Presentation

Interactive buttons and keyboards

Interactive Approval

Approval via chat interactions