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.
});

Back to top

A community package, not affiliated with or endorsed by Laravel. Released under the MIT licence.

This site uses Just the Docs, a documentation theme for Jekyll.