Secure Approval Backend

Durable, actor-authorised approval backend with SQLite persistence, unguessable IDs, and no LLM classifier in the decision path

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

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

Diagram

Secure Approval Flow

approve / deny

🤖 Tool Request

🔒 Presentation\nApprovalBackend

💾 SQLite\nApprovalStore

📱 Channel\nButtons

👤 Authorised\nActor

🔑 Approval ID\nCallback

✅ Decision

How It Works

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

Diagram
sequenceDiagram
    participant User
    participant Agent
    participant SecureBackend
    participant Actor

    User->>Agent: Risky tool call
    Agent->>SecureBackend: Persist request + approval ID
    SecureBackend->>Actor: Prompt authorised approver
    Actor-->>SecureBackend: Approve / deny by ID
    SecureBackend-->>Agent: Decision
    Agent-->>User: Result or blocked

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:

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

    bash
    export PRAISONAI_APPROVAL_ACTORS="user123,user456,admin789"

    Values are comma-separated actor (user) IDs from your chat platform.

  3. Programmatic Usage
    python
    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:

    python
    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 approve✅ PRAISONAI_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

Storage

Approvals persist to SQLite at:

~/.praisonai/state/approvals.sqlite

Override with PRAISONAI_HOME:

bash
export PRAISONAI_HOME=/var/lib/praisonai
# → /var/lib/praisonai/state/approvals.sqlite

API Reference

python
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

Always set PRAISONAI_APPROVAL_ACTORS

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.

Call rehydrate() at startup

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.

Use SQLite persistence for production

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.

Scope actor IDs to one platform

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.


Related