Tool Approval

Declare that a tool needs human sign-off before it runs

Declare a tool needs human approval directly on the @tool decorator; every runtime honours it.

python
from praisonaiagents import Agent, tool

@tool(approval=True)
def delete_account(user_id: str) -> str:
    """Permanently delete a user account."""
    return f"Deleted {user_id}"

agent = Agent(
    name="Support",
    instructions="Help users. Ask before doing anything destructive.",
    tools=[delete_account],
)
agent.start("Delete account u_42")

The tool carries its own approval requirement — the agent pauses and asks a human before delete_account runs.

Diagram

required

✅ allow

❌ deny

not required

🤖 Agent

🔧 @tool call

🛡️ ApprovalRegistry

❓ Ask Human

▶️ Execute

⏹️ Skip

Quick Start

  1. One-line, high-risk default

    approval=True registers the tool at "high" risk:

    python
    from praisonaiagents import Agent, tool
    
    @tool(approval=True)
    def refund(order_id: str) -> str:
        """Issue a refund on an order."""
        return f"Refunded {order_id}"
    
    agent = Agent(
        name="Support",
        instructions="Help support agents process refunds.",
        tools=[refund],
    )
    agent.start("Refund order O-101")
  2. Explicit risk level

    Pass a string to choose the level explicitly:

    python
    from praisonaiagents import tool
    
    @tool(approval="critical")
    def deploy(env: str) -> str:
        """Deploy to an environment."""
        return f"Deployed to {env}"
  3. Renamed tool

    Registration keys off the resolved tool name — not the function name:

    python
    from praisonaiagents import tool
    
    @tool(name="danger_op", approval=True)
    def internal_fn(payload: str) -> str:
        """Run a dangerous operation."""
        return f"Ran {payload}"
    
    # The approval requirement is registered under "danger_op".

How It Works

Registration happens once at import time; enforcement happens on every call.

Diagram
HumanApprovalRegistryToolLLMAgentUserHumanApprovalRegistryToolLLMAgentUserAt import timeAt call timeadd_approval_requirement(name, risk_level)“Delete account u_42”Prompt + tool schemaCall delete_account(u_42)is_approval_required(“delete_account”)?Yes (high)Allow delete_account?Yes / NoExecute (or skip)
StageWhat happens
ImportThe decorator calls add_approval_requirement(tool.name, tool.risk_level) on the global registry
CallThe agent checks is_approval_required(name) before running the tool
AskThe active approval backend (console by default) asks a human
RunThe tool runs only after approval; otherwise it is skipped

Choose your level

Diagram

Irreversible / destructive

Writes / mutates state

Read-only but sensitive

Cosmetic / low risk

Safe

How dangerous is the tool?

approval=’critical’

approval=’high’ or True

approval=’medium’

approval=’low’

Omit the parameter

Scope of a skill grant

When a skill’s allowed-tools frontmatter grants approval for a tool, the grant is scoped to the agent instance that activated the skill. It does not leak onto other agents in the same process — even other unnamed agents that share the default display name "Agent". Each Agent gets a unique per-instance approval-scope id at construction, and skill grants are keyed by that id rather than the display name.

python
from praisonaiagents import Agent, tool

@tool(approval=True)
def destructive_delete(path: str) -> str:
    """Delete a file."""
    return f"Deleted {path}"

reviewer = Agent(skills=["./skills/file-reviewer"])   # grants read_file via allowed-tools
deleter  = Agent(tools=[destructive_delete])          # separate agent; still gated

# `deleter` cannot inherit `reviewer`'s skill grant, even though both default to name="Agent".

Configuration Options

Accepted values for approval (canonical):

ValueEffect
None (default)No change. Tool runs without approval.
TrueTool registered at "high" risk level.
"critical"Tool registered at "critical" risk level.
"high"Tool registered at "high" risk level.
"medium"Tool registered at "medium" risk level.
"low"Tool registered at "low" risk level.
Any other stringValueError raised at decoration time (e.g. "critial" — typo guard).

Deprecated alias: requires_approval

requires_approval= is the old spelling. It still works but emits a DeprecationWarning — migrate to approval=.

python
from praisonaiagents import tool

@tool(requires_approval=True)          # works, but warns
def legacy_delete(user_id: str) -> str:
    """Delete a user account."""
    return f"Deleted {user_id}"
# DeprecationWarning: use approval= instead of requires_approval=
CallResult
@tool(approval=True)"high" risk. No warning.
@tool(approval="critical")"critical". No warning.
@tool(requires_approval=True)"high". DeprecationWarning.
@tool(requires_approval=False) (explicit)No approval. Still warns — any use of the old spelling nudges migration.
@tool(approval="critical", requires_approval="low")Both given → approval wins → "critical". DeprecationWarning.
@tool (neither)No approval. No warning.

Attributes on the returned FunctionTool:

AttributeTypeMeaning
.approval`bool \str \None`Canonical. The resolved approval value.
.requires_approval`bool \str`Deprecated alias. Populated with the same resolved value so existing readers keep working.
.risk_level`str \None`Resolved level: "high" when True; the string itself when a string; None when unset

Common Patterns

Mark one tool critical alongside safe tools

python
from praisonaiagents import Agent, tool

@tool
def get_balance(account_id: str) -> str:
    """Read an account balance."""
    return f"{account_id}: $1,240.00"

@tool(approval="critical")
def wire_transfer(account_id: str, amount: float) -> str:
    """Wire money out of an account — irreversible."""
    return f"Wired ${amount} from {account_id}"

agent = Agent(
    name="Finance",
    instructions="Help with account questions. Confirm before moving money.",
    tools=[get_balance, wire_transfer],
)
agent.start("What's the balance on acct-9 and wire $500 out")

Rename a tool and keep it gated

python
from praisonaiagents import tool

@tool(name="prod_deploy", approval="critical")
def deploy(env: str) -> str:
    """Deploy to production."""
    return f"Deployed to {env}"

# The registry key is "prod_deploy" — the name the LLM and agent see.

Compose with agent-level approval config

The decorator declares what needs approval; the agent config decides how to ask.

python
from praisonaiagents import Agent, tool

@tool(approval=True)
def refund(order_id: str) -> str:
    """Issue a refund."""
    return f"Refunded {order_id}"

agent = Agent(
    name="AsyncSupport",
    instructions="Process refunds.",
    tools=[refund],
    approval={"backend": "slack"},   # decorator says gate it; agent says ask via Slack
)
agent.start("Refund order O-101")

User interaction flow

A real run looks like this:

  1. You run the agent: agent.start("Delete account u_42").
  2. The LLM decides to call delete_account.
  3. The console approval prompt appears with five choices: [o] once, [s] session, [a] always, [n] no, [d] deny & redirect.
  4. You pick — the tool runs, or is skipped.

See Interactive Tool Approval for the full prompt semantics.


Best Practices

Gate anything that writes or deletes

Prefer approval=True for any tool that writes, mutates, or deletes state. True registers at "high" — safe default for destructive actions.

Reserve 'critical' for irreversible operations

Use 'critical' only for irreversible operations. Critical tools always re-prompt regardless of the argument cache, so they never slip through a cached grant. See the approval cache.

Keep the parameter on the definition, not the agent

Declare approval on the tool itself. The tool then travels safely between agents — no agent can accidentally use it ungated.

Use a non-console backend for async agents

approval on the decorator does not wire a backend — console stays the default. For async agents, combine with Agent(approval={"backend": "slack"}) or another non-console backend.


Related