Approval Workflows¶
Any permission on a SecurityContext can be routed through an approval
backend before the action actually runs — this is how you put a human (or
another agent, or a webhook, or anything else) in the loop for sensitive
operations like DELETE or EXECUTE, without hard-coding that logic into
every tool.
How it fits together¶
Set require_approval_for on the context, and pass an approval backend:
from langchain_mero_tools import SecurityContext, PathEntry, Permission, CLIApproval
ctx = SecurityContext(
paths=[
PathEntry(
path="./workspace",
slug="root",
allowed_permission=Permission.READ | Permission.WRITE | Permission.DELETE,
),
],
require_approval_for=Permission.WRITE | Permission.DELETE,
approval=CLIApproval(), # blocking terminal y/N prompt
)
When guard() (see Core Concepts)
sees that the permission being requested is in require_approval_for, it
builds an ApprovalRequest and calls ctx.approval.request(req). If that
returns True, the action proceeds; if False, it raises
ApprovalDeniedError, which the tool turns into a "Denied: ..." string.
If require_approval_for is set but approval is None, the action is
denied — approval requirements fail safe, not fail open.
ApprovalRequest¶
Every backend receives the same request object:
@dataclass
class ApprovalRequest:
action: str # e.g. "WRITE", "DELETE", "EXECUTE"
detail: str # e.g. the path or command being acted on
requester: str = "agent" # the SecurityContext's name
context: dict = field(default_factory=dict) # extra structured info
The four built-in backends¶
| Backend | Use case |
|---|---|
CLIApproval() |
Local dev — blocking input() prompt in the terminal. |
InterruptApproval() |
LangGraph interrupt() — pauses the graph, resumes via Command(resume=...) from your own UI/API. See LangGraph Integration. |
CallbackApproval(fn) |
Wrap anything callable — a webhook, a Slack bot, a database poll, a custom UI. |
AutoApprove() / AutoDeny() |
Tests, fully-trusted internal contexts, or an explicit fail-safe default. |
All four implement the same tiny interface — request(req: ApprovalRequest)
-> bool — so they're fully interchangeable.
CLIApproval¶
from langchain_mero_tools import CLIApproval
approval = CLIApproval(auto_deny_on_eof=True) # default
Prints a prompt like:
and blocks on input(). Only y/yes (case-insensitive) returns True.
If stdin hits EOF (e.g. running non-interactively), it auto-denies by
default; pass auto_deny_on_eof=False if you'd rather the underlying
EOFError propagate.
InterruptApproval¶
Requires langgraph installed (pip install -e ".[langgraph]", see
Installation).
Calls LangGraph's interrupt() with a structured payload describing the
request, pausing the graph run entirely — no action is taken until your
own application resumes it with a decision. Full walkthrough, including a
complete working example: LangGraph Integration.
CallbackApproval¶
from langchain_mero_tools import CallbackApproval
def ask_slack(req):
return slack_bot.ask(channel="#approvals", question=f"{req.requester}: {req.action} {req.detail}")
approval = CallbackApproval(ask_slack)
Wraps any Callable[[ApprovalRequest], bool] — the simplest way to plug in
a custom synchronous decision source (a webhook call, a database poll
loop, anything).
AutoApprove / AutoDeny¶
from langchain_mero_tools import AutoApprove, AutoDeny
test_ctx = SecurityContext(..., require_approval_for=Permission.DELETE, approval=AutoApprove())
locked_ctx = SecurityContext(..., require_approval_for=Permission.DELETE, approval=AutoDeny())
AutoApprove() is useful in tests or fully-trusted internal contexts where
you want the shape of an approval-gated workflow without a human in the
loop. AutoDeny() is useful as an explicit, self-documenting fail-safe
default — clearer than leaving approval=None when you specifically want
"never allow this, ever" to be visible in the config.
Writing your own backend¶
The interface is one method — anything with a matching request method
works, no base class required (it's a typing.Protocol):
from langchain_mero_tools import ApprovalRequest
class MySlackApproval:
def request(self, req: ApprovalRequest) -> bool:
return my_slack_bot.ask(req.requester, req.action, req.detail)
ctx = SecurityContext(..., approval=MySlackApproval())
Chaining backends with ChainedApproval¶
ChainedApproval tries backends in order and returns the first
definitive result. A backend signals "not my call, ask the next one"
by raising NotImplementedError instead of returning False — this is
exactly how manager/worker delegation
is built: try the manager's own auto-decision first, fall back to a human
if the manager can't decide.
from langchain_mero_tools import ChainedApproval, CLIApproval, AutoDeny
approval = ChainedApproval([
some_fast_automated_check, # may raise NotImplementedError to defer
CLIApproval(), # falls back to a human
])
If every backend in the chain defers (raises NotImplementedError),
ChainedApproval raises ApprovalTimeoutError rather than silently
returning False — a chain that can't resolve a request is a
configuration problem worth surfacing loudly, not swallowing.
See also¶
- Manager/Worker Delegation — the main
real-world use of
ChainedApproval, where a manager agent can approve on behalf of workers within its own scope. - LangGraph Integration — full working
example of
InterruptApprovalpausing and resuming a graph. - Core Concepts — how approval fits into the
guard()flow every tool call goes through.