Skip to main content
Some actions are too consequential to run on the model’s say-so. An approver is the human in the loop: when a policy or risk tier escalates a tool call, the approver decides yes or no before the tool runs. Approval sits inside the tool chokepoint. It never touches the model or your business logic, and it adds nothing to calls that are not escalated.

Where approval fits

The before_tool hook on Governance runs on every tool call. It scores risk, evaluates policy, and only then consults the approver. A call reaches the approver when either condition holds:
  • The policy attaches a REQUIRE_APPROVAL obligation to its decision.
  • The call’s risk tier is at or above escalate_at (default RiskTier.HIGH).
If neither holds, the call is allowed and the approver is never invoked. Everything the approver sees has already survived policy: a FORBID decision is denied outright and never reaches approval.
escalate_at makes risk gate the decision, not just annotate it. Any permitted action at or above that tier is sent to the approver, so an unprofiled high-risk or side-effecting tool is denied by default unless a human approves. Set escalate_at=None to disable risk-driven escalation and rely on policy obligations alone.

The Approver protocol

An approver is any object with a single method. Approver is a runtime_checkable Protocol, so you do not need to subclass anything: match the shape and it works.
review returns True to allow the call and False to reject it. It must answer immediately. If a human sign-off cannot happen inline, use a DurableApprover instead, which suspends the run rather than blocking a thread. See Durable approval.

ApprovalRequest

review receives one argument, a frozen ApprovalRequest, describing the exact action awaiting a decision.
For risk or policy escalation, reason is "policy/risk requires approval". Use these fields to render a prompt, post to a review channel, or key an audit record.

Built-in approvers

DenyAll

Rejects everything. The fail-closed default.

CallbackApprover

Delegates the decision to a callable you supply.

AutoApprove

Approves everything. Development and testing only.

DurableApprover

Suspends the run for out-of-band, hours-later sign-off.

DenyAll (the default)

Governance defaults its approver field to DenyAll. If you configure escalation but never wire an approver, every escalated call is rejected. This is deliberate: an unattended agent cannot approve its own high-risk actions.
Fail-closed is the whole point. If you see escalated calls being blocked with “Approval rejected”, check whether you left the approver at its DenyAll default.

CallbackApprover

Wrap any Callable[[ApprovalRequest], bool]: a console prompt, a Slack round-trip, a queue consumer. The return value is coerced with bool.
Your callback must return promptly, since it runs inline on the tool call. A callback that itself waits on a human for minutes will hold the run’s thread the whole time; reach for durable approval there.

AutoApprove

Approves every request. Convenient for tests and local development where you want the escalation path exercised without a prompt.
Never ship AutoApprove to production. It disables the human gate entirely, turning every escalation into an automatic yes.

How a decision is handled

Inside before_tool, the approver’s verdict is recorded and enforced:
1

Approver is consulted

approver.review(ApprovalRequest(...)) is called only when the call needs approval.
2

Errors are treated as rejection

If review raises, the call is audited as rejected and blocked with "Approval failed for <tool>". An approver that fails does not fail open.
3

Verdict is audited

The decision (approved or rejected) is written to the tamper-evident audit log along with the tool, tier, and reasons.
4

Rejection blocks the tool

A False verdict returns a blocking ToolDecision ("Approval rejected for <tool>"); a True verdict lets the tool run.
The one exception is ApprovalRequired. A DurableApprover raises it to signal “no decision yet, suspend the run.” before_tool lets it propagate rather than treating it as an error, so the run can persist and resume later.
Every approval path is audited, including rejections and approver errors. See Audit for how those records are hash-chained and verified.

When inline approval is not enough

Approver.review is synchronous: it must answer in the moment. Real human sign-off can take minutes or hours, which no thread should block on. For that, use a DurableApprover, which looks up a prior decision in an ApprovalStore and, when none exists, records the pending request and raises ApprovalRequired so the run suspends and resumes once a human decides.

Durable approval

Suspend a run at the approval point and resume it hours later, TOCTOU-safe.

Policy

How REQUIRE_APPROVAL obligations and FORBID decisions are produced.