Approvable Laravel AI tools become durable harness approvals on their own.
use Laravel\Ai\Concerns\InteractsWithApprovals;
use Laravel\Ai\Contracts\Approvable;
class PublishArticle implements Tool, Approvable
{
use InteractsWithApprovals;
// ...
}
When the agent reaches that tool the run pauses, a checkpoint is written, and the worker exits normally. Nothing sits open holding a connection. Hours later, in a different process:
$run = Clutch::run($runId)->authorizeFor($user);
$run->approve(
approvalId: $approvalId,
reason: 'The content has been reviewed and may be published.',
);
Rejecting works the same way:
$run->reject(
approvalId: $approvalId,
reason: 'Do not publish this draft. Remove the unsupported claim first.',
);
A rejection comes back to the agent as a tool result it can respond to, rather than killing the run outright.
Decisions are idempotent. Repeating one returns the existing result. Trying to reverse a resolved decision raises ApprovalAlreadyResolved.
Endpoints for all of this ship with the package:
GET /api/clutch/runs/{run}/approvals
POST /api/clutch/runs/{run}/approvals/{approval}/approve
POST /api/clutch/runs/{run}/approvals/{approval}/reject
Notifications are yours to wire up:
Event::listen(ApprovalRequested::class, function (ApprovalRequested $event) {
$event->approval->session->participant->notify(
new AgentNeedsYou($event->approval)
);
});
Permission modes
use Clutch\Laravel\Enums\PermissionMode;
$session = Clutch::agent(PublishingAgent::class)
->for($user)
->permissions(PermissionMode::ApproveSensitive)
->create();
| Mode | Behavior |
|---|---|
DenyByDefault |
Only explicitly allowed tools may run |
ApproveSensitive |
Safe tools run, sensitive ones need approval |
ApproveAll |
Every state changing tool needs approval |
AllowAll |
No harness approval, for trusted environments |
Classify tools in configuration:
'permissions' => [
'tools' => [
'search_web' => 'read_only',
'draft_email' => 'reversible',
'send_email' => 'irreversible',
],
],
Or let a tool speak for itself:
class PublishArticle implements Tool, SensitiveTool
{
public function sensitivity(): ToolSensitivity
{
return ToolSensitivity::Irreversible;
}
}
A tool you have not classified counts as sensitive.
For the mode to apply, pass your tool list through Clutch::policy():
public function tools(): iterable
{
return Clutch::policy([
new SearchWeb,
new DraftEmail,
new PublishArticle,
]);
}
Laravel AI asks the agent for its tools on every turn and that list is usually rebuilt each time, so the harness cannot rewrite it afterward. The agent hands it over instead. make:clutch-agent writes this for you.
With that in place, a tool the mode denies is withheld from the agent completely. The model is never told it exists, since refusing after the fact would still have exposed the capability. A tool that needs sign off is marked approvable, which is what pauses the run and creates the durable approval. Outside a harness run Clutch::policy() does nothing at all, so the same agent still behaves normally when you prompt it directly through Laravel AI.
A tool that asks for approval on its own, through Laravel AI’s requireApproval(), is making its author’s call rather than the harness’s. AllowAll relaxes harness policy and leaves that alone.
A session can also name the tools it may use at all, which is coarser than the permission mode and applies before it:
Clutch::agent(SupportAgent::class)->onlyTools(['search_orders', 'draft_reply'])->create();
Clutch::agent(SupportAgent::class)->withoutTools(['issue_refund'])->create();
An allow list is absolute: anything absent is withheld whatever the mode would have permitted. Tools that survive the list are still subject to the mode, so an irreversible tool on an allow list still asks for approval.
Laravel authorization policies apply regardless of mode. Application rules can layer on as well, and the most restrictive answer wins:
app(PolicyEngine::class)->extend(function (ToolInvocation $invocation, ToolSensitivity $sensitivity) {
return $invocation->tenantId && Team::find($invocation->tenantId)?->on_trial
? PolicyDecision::requireApproval('Trial teams review every write.')
: null; // Defer to the other rules.
});