Gateway Approval Durability

Survive gateway restarts with persisted pending approvals using the secure approval backend

Use --approval secure so pending approvals survive a gateway restart — the SQLite-backed PresentationApprovalBackend persists every decision and rehydrates outstanding requests on startup.

Diagram
graph LR
    subgraph "Gateway Approval Durability"
        Agent[🤖 Agent] --> Backend[🔒 PresentationApproval\nBackend]
        Backend --> Store[(💾 SQLite\nStore)]
        Store --> Restart[🔄 Restart]
        Restart --> Rehydrate[📥 rehydrate()]
        Rehydrate --> Resolve[✅ Same request\nresolvable]
    end

    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef process fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef storage fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef result fill:#10B981,stroke:#7C90A0,color:#fff

    class Agent agent
    class Backend,Rehydrate process
    class Store storage
    class Restart,Resolve result

Quick Start

  1. Set required env vars (CLI only)
    bash
    export PRAISONAI_APPROVAL_ACTORS="your-user-id"
    export PRAISONAI_HOME=/var/lib/praisonai   # optional, overrides ~/.praisonai

    PRAISONAI_APPROVAL_ACTORS is required when using --approval secure from the CLI. For the Python API, pass allowed_actors= directly to PresentationApprovalBackend (see next step).

  2. Agent-centric example
    python
    import os
    from praisonai.bots import ApprovalStore, PresentationApprovalBackend
    from praisonaiagents import Agent
    
    store = ApprovalStore(path=os.path.expanduser("~/.praisonai/state/approvals.sqlite"))
    backend = PresentationApprovalBackend(
        store=store,
        allowed_actors={"your-user-id"},
    )
    
    agent = Agent(
        name="Ops",
        instructions="You are a careful ops assistant. Ask before destructive commands.",
        approval=backend,
        tools=["execute_command"],
    )
    
    agent.start("Clean up /tmp/cache")
    # The gateway asks the human once. Restart the gateway — the pending request
    # is rehydrated from SQLite and the human can still Allow or Deny.
  3. CLI usage
    bash
    praisonai run --approval secure agent.py

How It Works

Diagram
HumanSQLite Store (ApprovalStore)PresentationApprovalBackendAgentHumanSQLite Store (ApprovalStore)PresentationApprovalBackendAgentgateway restartsrequest_approval(request)persist(approval_id, request, expires_at)prompt for decisionrehydrate() on startupoutstanding pending approvalsAllow / Denyresolve + audit trail writeApprovalDecision
StepWhat happens
persistRequest is written to SQLite before the human is prompted
rehydrate()On startup, outstanding requests are restored with their original expiry
resolveEvery decision (approved / denied / expired) is written to the audit trail
actor checkOnly actors listed in PRAISONAI_APPROVAL_ACTORS may approve

Configuration

SettingValueNotes
PRAISONAI_APPROVAL_ACTORSComma-separated actor IDsRequired for --approval secure. Approvers not on this list are rejected.
PRAISONAI_HOMEAny pathOverrides the state root. Default: ~/.praisonai

Default persistence paths

FileContents
~/.praisonai/state/approvals.sqlitePending approvals + audit trail (approved / denied / expired)

Override the root with PRAISONAI_HOME:

bash
export PRAISONAI_HOME=/var/lib/praisonai
# Store will be created under /var/lib/praisonai/state/approvals.sqlite

Programmatic Override

python
from pathlib import Path
from praisonai.bots import ApprovalStore, PresentationApprovalBackend
from praisonaiagents import Agent
from praisonaiagents.approval import ApprovalConfig

store = ApprovalStore(path="/var/lib/praisonai/state/approvals.sqlite")
backend = PresentationApprovalBackend(
    store=store,
    allowed_actors={"user-123", "ops-team"},
    timeout=600,
)

agent = Agent(
    name="Ops",
    instructions="Ask before destructive commands.",
    approval=ApprovalConfig(backend=backend, all_tools=True),
)

PresentationApprovalBackend constructor parameters:

ParameterTypeDefaultDescription
store`ApprovalStore \None`NoneDurable SQLite store; None means in-memory only
allowed_actors`Iterable[str] \None`NoneActor IDs that may approve; None allows any actor (legacy)
channel_send_func`callable \None`NoneAsync function to send approval prompt to a channel
target`str \None`NoneDefault channel/chat ID
timeoutfloat300.0Seconds to wait for a decision before failing closed

What Happens on Restart

  • No live awaiter: the awaiting agent call was lost with the previous process. Rehydrated requests stay resolvable via the gateway UI or API. The audit trail records the terminal state.
  • Expiry is preserved: rehydrate() filters by stored expires_at, so a nearly-expired approval is not inadvertently extended.
  • Custody survives restart: authorized_reviewers on a pending request is persisted and rehydrated by the same SQLite store, so per-request reviewer bindings survive a gateway restart. See Per-request reviewer custody.
  • Fail-open startup: a corrupted store logs the error and returns 0 — it never blocks gateway startup.

What Happens on /stop or Supersede

Pending approvals bound to a superseded turn are cancelled fail-closed via cancel_for_generation() — the awaiting tool call unwinds promptly with reason="cancelled". A resolution that arrives after the supersede is dropped and recorded with reason="superseded" in the audit trail, so a late reviewer decision never fires a stale tool or delivers a stale reply.

This binding is a no-op for approvals that carry no session_id/run_generation. See Approval Turn Liveness.


Best Practices

Enable in every production deployment

Use --approval secure on the CLI (or pass a PresentationApprovalBackend instance via approval= in Python) in your deployment environment. Without a durable store, a gateway restart drops all pending approvals — agents waiting for a human decision receive a timeout/deny.

Always set PRAISONAI_APPROVAL_ACTORS

The secure backend requires at least one authorised actor. Omitting PRAISONAI_APPROVAL_ACTORS causes a startup error by design — it prevents a misconfigured deployment from silently accepting approvals from any actor.

Rotate SQLite files with host rotation

Grants are stored per-host. When rotating to a new host, copy ~/.praisonai/state/*.sqlite to preserve in-flight approvals — do not leave agents in a state where they re-prompt unnecessarily in production.

Combine with actor allow-lists for defence-in-depth

PRAISONAI_APPROVAL_ACTORS scopes who can approve. Pair this with network-level access controls on the gateway callback endpoint for defence-in-depth.


Related