Approval Protocol
Extensible tool-execution approval system for agents
Agents can require human approval before executing dangerous tools. Set approval=True on any agent to auto-approve, or pass a custom backend for console, webhook, Slack, or any approval channel.
from praisonaiagents import Agent
agent = Agent(name="assistant", instructions="Ask before destructive tools.", approval=True)
agent.start("List files in the current directory.")For unified ApprovalSpec configuration (YAML/CLI/Python), see the Approval documentation. Backend selection uses the same string values in
--approvaland YAMLbackend:fields.
The user enables approval on an agent; each dangerous tool call pauses until the configured backend allows or denies it.
Quick Start
- Auto-Approve (Bots / Trusted Envs)
python from praisonaiagents import Agent agent = Agent( name="bot", instructions="You are a helpful assistant.", approval=True ) agent.start("List files in current directory") - Custom Approval Backend
python from praisonaiagents import Agent from praisonaiagents.approval import ApprovalRequest, ApprovalDecision from typing import Optional class WebhookBackend: async def request_approval(self, request: ApprovalRequest) -> ApprovalDecision: # POST to your approval service return ApprovalDecision(approved=True, approver="webhook") def request_approval_sync(self, request: ApprovalRequest, timeout: Optional[float] = None) -> ApprovalDecision: return ApprovalDecision(approved=True, approver="webhook") agent = Agent( name="bot", instructions="You are a helpful assistant.", approval=WebhookBackend() ) agent.start("List files in current directory") - Default (Console Prompt)
python from praisonaiagents import Agent # No approval= param → dangerous tools prompt in terminal agent = Agent( name="interactive", instructions="You are a helpful assistant.", ) agent.start("Delete temporary files")
How It Works
| Step | What Happens |
|---|---|
| Agent backend | approval=True or custom backend on the Agent is checked first |
| Required? | Only tools in the dangerous-tools set need approval |
| Env check | PRAISONAI_AUTO_APPROVE=true skips all prompts |
| YAML check | Tools listed in YAML approve field are auto-approved |
| Already approved | Once approved in a session, no re-prompt |
| Backend | ConsoleBackend (default), AutoApproveBackend, or your custom |
ApprovalRequest fields
The request object a backend receives, describing the tool call awaiting approval.
| Field | Type | Default | Description |
|---|---|---|---|
tool_name | str | — | Name of the tool requesting approval. |
arguments | Dict[str, Any] | — | Arguments the tool will be called with. |
risk_level | str | "medium" | Risk classification (critical / high / medium / low). |
agent_name | Optional[str] | None | Name of the agent that triggered the call. |
session_id | Optional[str] | None | Session identifier for tracking. |
context | Dict[str, Any] | {} | Arbitrary context dict for backend-specific data. |
approval_id | str | auto | Stable correlation id that survives a process restart. |
authorized_reviewers | Optional[List[str]] | None | Reviewer identities allowed to resolve this request. Honoured by the gateway backend (see per-request reviewer custody); ignored by backends that do not implement reviewer custody. None lets any resolver resolve. |
liveness | Optional[Callable[[], bool]] | None | Zero-arg predicate returning True while the originating turn is still live. When present, the registry and the direct-backend path drop a resolution that arrives after the turn was stopped or superseded — fail-closed: the tool does not run and no session/always grant is persisted. None (the default) preserves the pre-#4950 behaviour: the request is treated as always live. Populated automatically by the SDK from the run’s cooperative cancel signal — application code does not set it. See Live-Authority Binding. |
ApprovalDecision fields
The decision object a backend returns. Denials can carry a feedback string to steer the agent’s next turn, and a decision may instead be a deferral carrying escalate=True that hands the call to a human.
| Field | Type | Default | Description |
|---|---|---|---|
approved | bool | — | Whether execution is allowed. |
reason | str | "" | Human-readable reason for the decision. |
modified_args | Dict[str, Any] | {} | Optional replacement arguments (empty = use originals). |
approver | Optional[str] | None | Who/what approved (user, system, webhook, …). |
metadata | Dict[str, Any] | {} | Backend-specific metadata (timestamps, IPs, …). |
scope | str | "once" | How long the decision is remembered: "once", "session", or "always". |
scope_pattern | Optional[str] | None | Reusable target/pattern to persist for always. |
feedback | Optional[str] | None | Free-text guidance supplied by the user when denying a tool call (via [d] deny & redirect on ConsoleBackend or the Rich/Textual reject dialog). When present on a denial, tool_execution.py folds it into the denied tool’s error_msg as course-correction for the next turn. None (default) keeps today’s plain-deny semantics. |
escalate | bool | False | When True, the decision is a deferral to a human, not a final verdict — the autonomous reviewer was uncertain and asks a person to decide instead of failing open or blindly denying. Enforced invariant: escalate=True always forces approved=False (fail-closed), so a consumer that ignores this flag keeps today’s safe deny-by-default; a human-in-the-loop backend may route escalated requests to a person. |
Configuration Options
Since PR #4878, the PRAISONAI_AUTO_APPROVE env rung is checked even when a backend is attached — not just for bare-SDK use.
Agent Parameter (Recommended)
| Value | Behavior |
|---|---|
approval=True | Auto-approve all dangerous tools for this agent |
approval=False / None | Use registry fallback (default: console prompt) |
approval=MyBackend() | Use a custom approval backend for this agent |
Other Methods
| Method | Scope | Use Case |
|---|---|---|
PRAISONAI_AUTO_APPROVE=true | All agents | CI/CD, testing |
get_approval_registry().set_backend(backend) | All agents | Global policy |
get_approval_registry().set_backend(backend, agent_name="x") | Single agent | Registry-level per-agent control |
YAML approve: [tool1, tool2] | Per-task | Declarative configs |
Built-in Backends
| Backend | Import | Behavior |
|---|---|---|
AutoApproveBackend | from praisonaiagents.approval.backends import AutoApproveBackend | Always approves |
ConsoleBackend | from praisonaiagents.approval import ConsoleBackend | Rich terminal prompt (default) |
AgentApproval | from praisonaiagents.approval import AgentApproval | Delegates to another AI agent |
SlackApproval | from praisonai.bots import SlackApproval | Slack Block Kit + polling |
TelegramApproval | from praisonai.bots import TelegramApproval | Telegram inline keyboard + polling |
DiscordApproval | from praisonai.bots import DiscordApproval | Discord embed + text reply polling |
WebhookApproval | from praisonai.bots import WebhookApproval | POST to webhook + poll status |
HTTPApproval | from praisonai.bots import HTTPApproval | Local web dashboard |
GatewayApprovalBackend | from praisonai_bot.gateway.gateway_approval import GatewayApprovalBackend | Queue approvals into the gateway control plane |
CallbackBackend | from praisonaiagents.approval import CallbackBackend | Wraps a legacy callback function |
TypeScript imports
The four core backends have TypeScript parity in praisonai-ts — import them flat from praisonai. See Approval Backends (TypeScript).
| Backend | Python import | TypeScript import |
|---|---|---|
AutoApproveBackend | from praisonaiagents.approval.backends import AutoApproveBackend | import { AutoApproveBackend } from 'praisonai' |
ConsoleBackend | from praisonaiagents.approval import ConsoleBackend | import { ConsoleBackend } from 'praisonai' |
AgentApproval | from praisonaiagents.approval import AgentApproval | import { AgentApproval } from 'praisonai' |
CallbackBackend | from praisonaiagents.approval import CallbackBackend | import { CallbackBackend } from 'praisonai' |
Install a TypeScript backend with setApprovalBackend(backend, manager?); the chat/channel backends (SlackApproval, TelegramApproval, …) are Python/CLI-only.
GatewayApprovalBackend webhook payload
GatewayApprovalBackend(notify_url="https://your-consumer/hook") POSTs a JSON payload to the URL:
{
"request_id": "f3a2…",
"status": "pending",
"tool_name": "execute_command",
"arguments": {"command": "rm -rf /tmp/cache"},
"agent_name": "ops",
"risk_level": "high"
}The status field (added in PR #5916) says which lifecycle event fired:
status | When it fires | What the consumer should do |
|---|---|---|
"pending" | Once, when the request is created | Show a live Allow/Deny card |
"timed_out" | At most once, when nobody resolved the request within timeout | Mark the card as not run and disable its buttons |
- The
"timed_out"POST is sent only after the"pending"POST has completed, so it never arrives before the card it settles. - No
notify_url⇒ no webhook is sent, on creation or on timeout. - A human Approve/Deny does not fire a webhook — only
"pending"and"timed_out"are emitted. - The agent still receives a fail-closed DENY on timeout (
approver="gateway:timeout").
Gateway-wired approvals
Setting channels.<platform>.allow_shell: true with auto_approve_shell: false on a gateway channel wires one of these backends automatically — no Python required. If none of them resolve, the gateway installs a CallbackBackend that denies shell tools (execute_command, shell_command, acp_execute_command) so shell never silently auto-approves. See Fail-closed behaviour.
The gateway also routes through this same backend precedence when auto_approve_shell: true is downgraded on exposed / multi-user surfaces (see Bot Shell Execution → Exposure-aware auto-approval). The precedence itself is unchanged — only the trigger.
The routing precedence, exactly as implemented:
approval_mode == "gateway" → GatewayApprovalBackend()
approval_mode == "http" → HTTPApproval(host, port)
approval_mode == "webhook" OR
approval_webhook_url is set → WebhookApproval(webhook_url)
↳ webhook mode with NO url:
warn + fall through to gateway
channel_type == "slack" + resolved id → SlackApproval(...)
channel_type == "telegram" + resolved id → PresentationApprovalBackend(target=chat_id)
channel_type == "discord" + resolved id → DiscordApproval(...)
otherwise → GatewayApprovalBackend() (fallback)On gateway Telegram channels, the gateway installs PresentationApprovalBackend and renders / resolves taps through the channel adapter — not TelegramApproval, whose own getUpdates loop would conflict with the running bot. See Telegram Durable Approval → Gateway mode.
Full field reference, per-platform ID resolution, env vars, and recipes.
Custom Backend
Implement request_approval (async) and optionally request_approval_sync to create any approval channel:
from typing import Optional
from praisonaiagents import Agent
from praisonaiagents.approval import ApprovalRequest, ApprovalDecision
class SlackBackend:
async def request_approval(self, request: ApprovalRequest) -> ApprovalDecision:
approved = await self._ask_slack(
f"Approve {request.tool_name} for {request.agent_name}?"
)
return ApprovalDecision(
approved=approved,
approver="slack",
metadata={"channel": "#approvals"},
)
def request_approval_sync(self, request: ApprovalRequest, timeout: Optional[float] = None) -> ApprovalDecision:
# Sync fallback for non-async tool execution.
# `timeout` is optional: None waits indefinitely, > 0 is a per-call limit.
# Never mutate a shared `self._timeout` here — use the passed value.
return ApprovalDecision(approved=True, approver="slack")
agent = Agent(
name="slack-bot",
instructions="You are a helpful assistant.",
approval=SlackBackend()
)A custom backend may also return ApprovalDecision(approved=False, escalate=True) to signal a deferral to a human. Consumers that only check approved still fail-closed correctly, since escalate=True forces approved=False.
Slack Approval
Route tool approvals to Slack — get a rich message with tool details and reply yes or no to approve or deny.
- Install
bash pip install praisonai - Set Token
bash export SLACK_BOT_TOKEN=xoxb-your-token - Use It
python from praisonaiagents import Agent from praisonai.bots import SlackApproval agent = Agent( name="assistant", instructions="You are a helpful assistant.", tools=[execute_command], approval=SlackApproval(channel="#approvals") ) agent.start("Delete old log files")
Configuration
| Parameter | Default | Description |
|---|---|---|
token | SLACK_BOT_TOKEN env | Slack bot token (xoxb-...) |
channel | Bot’s own DM | Channel ID, name, or user ID |
timeout | 300 (5 min) | Seconds to wait for response |
poll_interval | 3.0 | Seconds between polls |
allowed_approvers | SLACK_APPROVERS env, else None | Iterable of Slack user IDs (or comma-separated string) permitted to approve/deny. None = any responder (legacy). Set this for shared channels. |
store | None | Optional durable ApprovalStore. When set, the pending approval is persisted before polling and the decision recorded, so an outstanding approval survives a restart. Call backend.rehydrate() on startup to recover in-flight approvals. None keeps the legacy in-memory-only behaviour. |
Make Slack approvals restart-safe by adding store=:
from praisonai.bots import SlackApproval, ApprovalStore
slack = SlackApproval(
channel="#approvals",
store=ApprovalStore(path="~/.praisonai/state/approvals.sqlite"),
)Approval Keywords
| Action | Keywords |
|---|---|
| Approve | yes, y, approve, approved, ok, allow, go, proceed, confirm |
| Deny | no, n, deny, denied, reject, block, stop, cancel, refuse |
Cross-Platform: Telegram Bot → Slack Approval
You can use any bot platform for user interaction while routing approvals to Slack:
from praisonaiagents import Agent
from praisonai.bots import Bot, SlackApproval
agent = Agent(
name="assistant",
instructions="You are a helpful assistant.",
approval=SlackApproval(channel="U0ABPEV1HK8") # DM to admin
)
# Users talk via Telegram; dangerous tools approved in Slack
bot = Bot("telegram", agent=agent)
bot.run()Telegram Approval
Route tool approvals to Telegram — get a message with inline keyboard buttons.
from praisonaiagents import Agent
from praisonai.bots import TelegramApproval
agent = Agent(
name="assistant",
tools=[execute_command],
approval=TelegramApproval(chat_id="YOUR_CHAT_ID") # token from TELEGRAM_BOT_TOKEN env
)| Parameter | Default | Description |
|---|---|---|
token | TELEGRAM_BOT_TOKEN env | Telegram bot token |
chat_id | _(required)_ | Chat ID to send approvals to |
timeout | 300 (5 min) | Seconds to wait for response |
poll_interval | 2.0 | Seconds between polls |
allowed_approvers | TELEGRAM_APPROVERS env, else None | Iterable of Telegram user IDs (or comma-separated string) permitted to tap Approve/Deny or reply with keywords. None = any responder (legacy). Set this for group chats. |
store | None | Optional durable ApprovalStore. When set, the pending approval survives a restart; call backend.rehydrate() on startup to recover it. None keeps the legacy in-memory-only behaviour. |
Discord Approval
Route tool approvals to Discord — get a rich embed and reply yes or no.
from praisonaiagents import Agent
from praisonai.bots import DiscordApproval
agent = Agent(
name="assistant",
tools=[execute_command],
approval=DiscordApproval(channel_id="YOUR_CHANNEL_ID") # token from DISCORD_BOT_TOKEN env
)| Parameter | Default | Description |
|---|---|---|
token | DISCORD_BOT_TOKEN env | Discord bot token |
channel_id | _(required)_ | Channel ID to send approvals to |
timeout | 300 (5 min) | Seconds to wait for response |
poll_interval | 3.0 | Seconds between polls |
allowed_approvers | DISCORD_APPROVERS env, else None | Iterable of Discord user IDs (or comma-separated string) permitted to reply Approve/Deny. None = any responder (legacy). Set this for shared channels. |
store | None | Optional durable ApprovalStore. When set, the pending approval survives a restart; call backend.rehydrate() on startup to recover it. None keeps the legacy in-memory-only behaviour. |
Webhook Approval
POST approval requests to any HTTP endpoint and poll for decisions. Ideal for enterprise dashboards, CI/CD, and custom integrations.
import os
from praisonaiagents import Agent
from praisonai.bots import WebhookApproval
agent = Agent(
name="assistant",
tools=[execute_command],
approval=WebhookApproval(
webhook_url="https://your-app.com/api/approvals",
headers={"Authorization": f"Bearer {os.environ['APPROVAL_WEBHOOK_TOKEN']}"},
),
)| Parameter | Default | Description |
|---|---|---|
webhook_url | APPROVAL_WEBHOOK_URL env | URL to POST requests to |
status_url | webhook_url/{request_id} | URL template for polling |
headers | {} | Extra HTTP headers (e.g. auth) |
timeout | 300 (5 min) | Seconds to wait |
poll_interval | 5.0 | Seconds between polls |
allowed_approvers | WEBHOOK_APPROVERS env, else None | Iterable of approver IDs (or comma-separated string) permitted to resolve. None = any responder (legacy). Set this when the webhook status endpoint is reachable by more than one trusted operator. |
store | None | Optional durable ApprovalStore. When set, the pending approval survives a restart; call backend.rehydrate() on startup to recover it. None keeps the legacy in-memory-only behaviour. |
Agent Approval
Delegate approval decisions to another AI agent. The reviewer sees the tool name and arguments as untrusted data and returns one of APPROVE, DENY, or ESCALATE.
from praisonaiagents import Agent
from praisonaiagents.approval import AgentApproval
# Security reviewer agent
reviewer = Agent(
name="security-reviewer",
instructions="Only approve low-risk read operations. Deny anything destructive.",
)
worker = Agent(
name="assistant",
tools=[execute_command],
approval=AgentApproval(approver_agent=reviewer),
)The reviewer prompt is hardened against prompt injection:
- The tool name and arguments are wrapped in an
<arguments>…</arguments>untrusted-data block; forged<arguments>/</arguments>tags in the tool name or values are neutralised to‹arguments›. - Command-like argument values (keys in
{command, cmd, commands, script, shell, code, source, program}) have any trailing# …shell comment stripped before the reviewer sees them; other args pass through unchanged. - The reviewer replies with one of
APPROVE/DENY/ESCALATE. Parsing is fail-closed: empty, ambiguous, mixed, or negated (“DO NOT APPROVE”) responses →DENY;ESCALATEyieldsApprovalDecision(approved=False, escalate=True).
The sanitiser only cleans the copy shown to the reviewer — the arguments the tool actually receives are unchanged.
| Parameter | Default | Description |
|---|---|---|
approver_agent | Auto-created | Agent instance to evaluate requests |
llm | gpt-4o-mini | LLM for default approver agent |
Handling ESCALATE
When the reviewer is uncertain it returns escalate=True — route the request to a human instead of guessing.
decision = await backend.request_approval(request)
if decision.approved:
run_tool()
elif decision.escalate:
# Reviewer deferred to a human — route to a HITL channel
route_to_human(request, decision)
else:
deny(decision.reason)See AgentApproval Hardening for the full security model.
HTTP Approval
Serve a local web dashboard for approvals. Open the URL in your browser and click Approve or Deny.
from praisonaiagents import Agent
from praisonai.bots import HTTPApproval
agent = Agent(
name="assistant",
tools=[execute_command],
approval=HTTPApproval(host="127.0.0.1", port=8899),
)
# When a tool needs approval, open http://127.0.0.1:8899/approve/<request_id>| Parameter | Default | Description |
|---|---|---|
host | 127.0.0.1 | Bind address |
port | 8899 | Port to listen on |
timeout | 300 (5 min) | Seconds to wait for response |
allowed_approvers | HTTP_APPROVERS env, else None | Iterable of approver IDs (or comma-separated string) permitted to resolve. None = any visitor (legacy). Set this whenever the dashboard is reachable by more than one trusted operator. |
store | None | Optional durable ApprovalStore. When set, the pending approval survives a restart; call backend.rehydrate() on startup to recover it. None keeps the legacy in-memory-only behaviour. |
Multi-Agent Example
Different agents can have different approval policies:
from praisonaiagents import Agent
from praisonaiagents.approval import AgentApproval
from praisonai.bots import SlackApproval, TelegramApproval, HTTPApproval
# Bot agent: auto-approve everything
bot = Agent(name="bot", approval=True)
# Interactive agent: console prompt (default)
interactive = Agent(name="interactive")
# Slack-approved agent
slack_agent = Agent(name="slack-approved", approval=SlackApproval(channel="#approvals"))
# Telegram-approved agent
telegram_agent = Agent(name="tg-approved", approval=TelegramApproval(chat_id="123456"))
# AI-reviewed agent (no human needed)
reviewer = Agent(name="reviewer", instructions="Only approve safe read operations")
ai_agent = Agent(name="ai-reviewed", approval=AgentApproval(approver_agent=reviewer))
# Local dashboard agent
http_agent = Agent(name="dashboard", approval=HTTPApproval(port=8899))Registry (Advanced)
For global or centralized approval control, use the registry directly:
from praisonaiagents import Agent, AutoApproveBackend
from praisonaiagents.approval import get_approval_registry
registry = get_approval_registry()
# Global auto-approve
registry.set_backend(AutoApproveBackend())
# Per-agent override via registry
registry.set_backend(AutoApproveBackend(), agent_name="trusted-bot")Dangerous Tools (Default)
These tools require approval by default:
| Tool | Risk Level |
|---|---|
execute_command | critical |
kill_process | critical |
execute_code | critical |
write_file | high |
delete_file | high |
move_file | high |
execute_query | high |
crawl | medium |
scrape_page | medium |
Add or remove requirements:
from praisonaiagents.approval import get_approval_registry
registry = get_approval_registry()
registry.add_requirement("my_dangerous_tool", risk_level="high")
registry.remove_requirement("crawl")ApprovalStoreProtocol
Optional persistence layer for pending approvals — orthogonal to approval backends. Implement persist, list_pending, and resolve to survive restarts.
from praisonaiagents.approval import ApprovalStoreProtocol, ApprovalRequest, ApprovalDecision
class MyStore:
async def persist(self, approval_id: str, request: ApprovalRequest, *, expires_at: float) -> None: ...
async def list_pending(self): ...
async def resolve(self, approval_id: str, decision: ApprovalDecision) -> None: ...The wrapper ships ApprovalStore (SQLite) in praisonai.bots. See Durable Approvals.
Live-Authority Binding
Since PraisonAI #4950, every approval that flows through the core ApprovalRegistry is bound to the liveness of the originating turn. If the user hits /stop (or a superseding turn takes over) while an approval is parked on a human — a Telegram tap, a Slack reply, a webhook decision, a local dashboard click — the resolution that lands afterwards is dropped fail-closed at the resolution boundary:
- The tool does not run.
- No
sessionoralwaysgrant is persisted for the abandoned turn. - The returned decision is
ApprovalDecision(approved=False, reason="Turn no longer live (stopped or superseded)").
This is inherited automatically by every backend that goes through the registry — the built-ins listed on this page, PresentationApprovalBackend, and any custom third-party backend. It also applies when a backend is attached directly to the Agent via approval=... (the direct-backend path).
Backward-compatibility
ApprovalRequest.liveness defaults to None, and both approve_sync / approve_async accept liveness=None. Existing code that constructs an ApprovalRequest by hand and calls the registry keeps the old always-live behaviour. A liveness probe that raises is treated as live (fail-open on the probe, never on the decision) so a buggy predicate can never wedge the approval path.
What the registry does
The predicate the SDK supplies follows the effective per-turn cancel token — the same authority an explicit cancel_token= argument or a /stop targets — and falls back to the agent’s interrupt controller when no per-turn token is registered. When neither is attached the request stays always-live, preserving today’s behaviour.
Context Isolation
Approvals are scoped to the Python context they are granted in. An approval granted inside an asyncio task, a copy_context().run(...) worker, or a thread spawned with asyncio.to_thread(...) stays in that worker — it does not appear in the parent task, and it does not appear in sibling workers copied from the same parent.
import asyncio
from praisonaiagents import Agent
agent = Agent(name="ops", instructions="Run shell commands", tools=["execute_command"])
async def main():
# Worker A approves `execute_command` for its own context only.
# Worker B still gets its own approval prompt — the approval does not leak.
await asyncio.gather(
asyncio.to_thread(agent.start, "list /tmp"),
asyncio.to_thread(agent.start, "list /var/log"),
)
asyncio.run(main())This is the behaviour you want for anything that fans out: a single human approval must not silently unlock every parallel branch.
Fixed in PraisonAI PR #5757. Before #5757, mark_approved mutated the set stored in the per-context approval cache in place; because contextvars.copy_context() copies the mapping and not the set, an approval granted in a child context appeared in the parent and in every sibling copied from it. Multi-threaded and asyncio.to_thread users of approval= therefore saw a sibling worker’s approval silently unlock their own tool call. The registry now stores a new set each time, so approvals stay in the context that granted them.
Garbage-collected agents are safe
Letting an Agent be collected without calling .close() is safe on any thread. Agent.__del__ releases the agent’s process-global registry grants (auto_approve_tool entries, session-scoped targets) but never writes the per-context approval cache — a finalizer can run on any thread, in any context, so touching a ContextVar from there would clobber an unrelated worker’s approvals.
Agent.close()/Agent.aclose()— called by your code on the context that owned the agent — still evict that context’s approval cache for the agent’s scope. Interactive behaviour is unchanged.Agent.__del__— called by the garbage collector on an unspecified thread — leaves every context’s cache alone, and reclaims only the process-global grants.
from praisonaiagents import Agent
# OK on any thread, in any context — finalizer only reclaims
# process-global grants, never writes a ContextVar.
agent = Agent(name="worker", instructions="…", tools=["execute_command"])
agent.start("…")
del agent
# Explicit close() still clears this context's approval cache for the agent's scope.
agent2 = Agent(name="worker", instructions="…", tools=["execute_command"])
try:
agent2.start("…")
finally:
agent2.close()Fixed in PraisonAI PR #5757. The pre-fix finalizer rewrote the per-context approval cache from inside the garbage collector, which could wipe an unrelated context’s approval cache.
Best Practices
Use approval=True for bot agents
Set approval=True directly on agent constructors for unattended bots. This is the simplest, most agent-centric approach.
Use environment variable for CI/CD
Set PRAISONAI_AUTO_APPROVE=true in your CI environment to avoid blocking on prompts during automated testing.
Create custom backends for production
Build webhook or messaging backends that route approvals to the right team. The async protocol supports long-running approval flows.
Use per-agent backends for multi-agent systems
Different agents may need different approval policies. Pass different backends to each agent’s approval= parameter.