Approval handlers for Decision.ask_
StandardPermissions(mode=Mode.DEFAULT) returns Decision.ask_(...)
when a tool is marked destructive=True. Without an approval handler,
ask falls back to deny. The agent never silently bypasses the
gate. Wire a handler to surface the decision to a human / Slack /
ticket queue:
from loomflow import Agent
from loomflow.core.types import ToolCall
async def approve_via_slack(call: ToolCall, user_id: str | None) -> bool:
"""Return True to allow the tool call, False to deny."""
msg_id = await slack.post(
channel="#approvals",
text=f"User {user_id} wants to run {call.tool}({call.args!r}). React 👍 to approve.",
)
reactions = await slack.wait_for_reaction(msg_id, timeout=300)
return "👍" in reactions
agent = Agent(
"...",
permissions=StandardPermissions(mode=Mode.DEFAULT),
approval_handler=approve_via_slack,
tools=[delete_user_data], # @tool(destructive=True)
)Failure-mode contract
A buggy approval flow must NEVER silently green-light a gated tool. That’s the whole point of routing destructive calls through the gate in the first place.
- Handler returns
False→ tool result isdeniedwithreason="approval declined"; the run continues, the model sees the denial in the next turn. - No handler wired (
approval_handler=None) →deniedwithreason="approval required; no approver". Same UX as before M10.4 . Single-tenant code that didn’t want approvals keeps working. - Handler raises → treated as deny + warning logged.
Last updated on