Laravel AI SDK v1.0 Needs a Tenant Boundary
Authenticated tenant: 41 Authenticated user: 842 Agent tool: FindInvoices Invoice returned: INV-1098 Invoice tenant: 17 The model found an invoice. The tool executed successfully. No exception appeared in the logs. The response was factually correct. It also exposed another customer’s data. This is

Authenticated tenant: 41 Authenticated user: 842 Agent tool: FindInvoices Invoice returned: INV-1098 Invoice tenant: 17 Enter fullscreen mode Exit fullscreen mode The model found an invoice.
The tool executed successfully. No exception appeared in the logs. The response was factually correct.
It also exposed another customer’s data.
This is the dangerous kind of AI failure because every component appears to work. The problem is not hallucination. It is missing tenant isolation.
Laravel AI SDK v1.0 gives PHP developers a first-party system for agents, conversation storage, streaming, tool approvals and middleware.
It can decide when to call a tool and validate the arguments against a schema. Laravel also supports runtime provider configuration for tenant-specific API keys.
Those capabilities reduce integration work. They do not decide which records the current tenant is allowed to access.
{ "tool": "find_invoice", "arguments": { "customer_id": 842 } } Enter fullscreen mode Exit fullscreen mode The arguments look valid. But customer_id is not an authorization boundary. IDs may collide across imported datasets, users may belong to multiple workspaces, and a malicious prompt may ask for a different account.
The tenant must come from authenticated application state, never from the prompt or model-generated arguments.
Start by representing the current boundary explicitly:
final readonly class TenantContext { public function __construct ( public int $tenantId, public int $userId, ) {} } Enter fullscreen mode Exit fullscreen mode Resolve it after authentication and authorization:
$context = new TenantContext ( tenantId: $request -> user () -> current_team_id, userId: $request -> user () -> id, ); $response = ( new SupportAgent ( $context )) -> forUser ( $request -> user ()) -> prompt ( $request -> string ( 'message' )); Enter fullscreen mode Exit fullscreen mode Calling forUser() is important because Laravel can associate stored conversations with a user. It is not the complete boundary when one user can access several tenants or workspaces.
The active tenant must still be authorized independently.
Pass TenantContext into every data-access tool through its constructor:
use App\Models\Invoice; use Illuminate\Contracts\JsonSchema\JsonSchema; use Laravel\Ai\Contracts\Tool; use Laravel\Ai\Tools\Request; use Stringable; final class FindInvoices implements Tool { public function __construct ( private TenantContext $context, ) {} public function description (): Stringable | string { return 'Find recent invoices for the authenticated account.'; } public function schema ( JsonSchema $schema ): array { return [ 'status' => $schema -> string (), ]; } public function handle ( Request $request ): Stringable | string { $invoices = Invoice:: query () -> where ( 'tenant_id', $this -> context -> tenantId ) -> where ( 'user_id', $this -> context -> userId ) -> when ( $request [ 'status' ]?? null, fn ( $query, $status ) => $query -> where ( 'status', $status ) ) -> select ([ 'id', 'status', 'total', 'issued_at' ]) -> latest () -> limit ( 10 ) -> get (); return $invoices -> toJson (); } } Enter fullscreen mode Exit fullscreen mode Notice what is missing from the schema: tenant_id.
The model cannot supply it, replace it or persuade the tool to ignore it. Even a successful prompt injection remains trapped inside the authenticated tenant boundary.
Laravel’s own guidance for production-safe database tools recommends the same underlying pattern: pass identity through application code, restrict visible columns, cap results and use a read-only database connection where possible.
Fixing the Eloquent query is necessary, but AI applications move data through more than one layer.
A cached answer generated for Tenant 41 must never be reusable by Tenant 17 simply because both asked the same question.
The most useful test is not “Does the agent find an invoice?”
It is “Can the agent retrieve an invoice belonging to someone else?”
it ( 'cannot return another tenant invoice', function () { $context = new TenantContext ( tenantId: $this -> tenantA -> id, userId: $this -> userA -> id, ); $foreignInvoice = Invoice:: factory () -> create ([ 'tenant_id' => $this -> tenantB -> id, ]); $result = ( new FindInvoices ( $context )) -> handle ( new Request ([ 'status' => $foreignInvoice -> status ])); expect ( $result ) -> not -> toContain ( $foreignInvoice -> id ); }); Enter fullscreen mode Exit fullscreen mode Add adversarial cases for guessed IDs, reused conversation IDs, poisoned retrieval metadata, delayed approvals and queue jobs executed after membership is revoked.
Laravel AI SDK v1.0 makes agents feel native to Laravel. That is valuable because tools, queues, models and tests can remain inside the same application.
The abstraction should not hide the security model.
The best Laravel development company for a multi-tenant AI product should be able to identify where tenant identity originates, how it reaches every tool and what prevents the model from changing it.
A prompt can tell an agent to respect tenant boundaries.
What tenant-isolation test would you run before connecting a Laravel agent to production data?