ARTICLE

Approving a Tool Is Not Approving Data: What Laravel AI SDK 1.0 Changes, and What It Doesn't

LaravelAIprivacy

Laravel AI SDK 1.0 shipped on September 23, and one of the headline features is the Approvable contract: a tool that implements it pauses the agent until a human approves the call, rejects it with a reason, or rewrites the arguments. It is the right thing to have in the framework. But after a year of building a product that sends customer images to a model, my reaction was colder than I expected: tool approval is a control over an action, and the thing that keeps me up at night is a control over data. Those are not the same boundary, and putting them in the same place is an elegant way to get it wrong.

What the contract actually does

The mechanism is clean. A tool declares Approvable, uses the InteractsWithApprovals trait, and when the model decides to call it the conversation stops and hands back the pending calls with the arguments the model picked:

use Laravel\Ai\Concerns\InteractsWithApprovals;
use Laravel\Ai\Contracts\Approvable;
use Laravel\Ai\Contracts\Tool;

class DeleteFile implements Approvable, Tool
{
    use InteractsWithApprovals;
    // ...
}

You resume by passing a decision per call: approve, reject with a reason the model gets to read, or edit the arguments before the tool runs. It works with prompt, stream, queue and the broadcast methods, so it survives the case where the agent runs on a queue and the approval arrives twenty minutes later from a different HTTP session. That part — persisting the state of a suspended agent — is the boring work nobody wants to rewrite, and it is the real reason to reach for the package.

The mental model it suggests, though, is: the agent proposes, the human confirms, the action runs. That is perfect for DeleteFile, for a refund, for an email going out. It is much weaker when the risky part is not what the tool does, but what the prompt carries with it.

In Miraviso the boundary sits lower down

In Miraviso, the virtual mirror for hair salons, the pipeline has two declared privacy treatments, because they genuinely are two different mechanisms.

The colour try-on runs entirely on the tablet: MediaPipe segmentation on-device, colour composited locally, no frame ever leaving the hardware. The haircut preview does not: that one needs a generative model, it runs on Gemini via Vertex AI in an EU region, it requires explicit consent from the person sitting in the chair, and the result is never written to disk.

Take that second path and try modelling it as an approvable tool. The approval would read something like: "the model wants to call generate_haircut_preview with this image — confirm?" But who confirms? The salon operator — the only person holding the tablet, and the only one the agent loop can question. And the consent I need is not theirs. It belongs to the client, who never touches the interface and who has to be able to say no before the camera does anything useful at all.

Hence the rule I ended up with: human approval inside the agent protects whoever is driving the agent. Consent to processing protects whoever is inside the data. If the only gate lives in the agent loop, I have built a confirmation dialog for the wrong person.

The gate that sits before the model

In my pipeline consent is not a turn in a conversation: it is session state, and the client cannot even compose a generative request until that state exists. The FastAPI endpoint that talks to Vertex refuses upstream, not after the fact, and not by trusting a flag the tablet sent:

@router.post("/preview/cut")
async def preview_cut(req: CutRequest, session: Session = Depends(current_session)):
    if not session.consent.generative_preview:
        raise HTTPException(403, "missing consent for generative preview")
    # from here on the frame only ever exists in memory
    ...

Three practical consequences, all more boring than a PHP contract and all more important:

I wrote about that last point separately, because it is a far more common failure than it looks: sensitive data ends up in logs almost always through a failed query or an APM trace, not through a line somebody wrote on purpose.

Where I would use it tomorrow

None of this is a criticism of the package; it is a division of labour. On the back-office side — the Laravel side, where customer records, appointments and invoicing live — an agent that proposes actions and waits for confirmation is exactly the right tool. Merging two duplicate customer records, moving a run of appointments, issuing a credit note: these are operations where the argument the model picked is precisely the thing I want to be able to correct, and being able to rewrite arguments before execution is worth more than being able to say no.

Worth flagging too: per-step middleware, which in 1.0 runs on every generation step instead of once per prompt. You get a PendingStep and can swap the model, strip tools or lower the token budget as the conversation goes on. For a solo founder paying for tokens out of pocket that is a concrete cost lever — dropping the expensive tool after its first use is now one line, and used to be an architecture problem.

The rule I am taking away, and it travels well beyond this SDK: when a framework hands you a gate, ask who is standing on the handle side. If the answer is not the person whose data is passing through, the gate is useful but it is not yours.

If you want more of how these things are built, the rest of the articles are here — and if you want to see code written with no framework at all, the logic games are all hand-written.

← All articles