Tenancy
The package works in a panel without tenancy: one workspace, the app's name, everyone's chats in one table. In a panel with tenancy it follows the workspace: the prompt names it, the budget counts it, the MCP endpoint carries it and tokens are bound to it.
The MCP path
Put {tenant} in the path so an external agent works inside one workspace:
// config/packstub-agents.php
'mcp' => [
'path' => 'mcp/{tenant}',
],
AuthenticateAgent then, after auth:sanctum:
- looks the workspace up by the panel's tenant slug attribute (
$panel->tenantSlugAttribute(), or the key); - checks the person's membership with
canAccessTenant()on the user model (404 otherwise); - checks that the token carries
tenant:{slug}(403 when it was issued for another workspace, even when the person is a member there); - sets the panel's guard to the token's user and calls
Filament::setTenant($tenant).
That last step fires Filament's TenantSet event, the same one a page request fires, so anything that listens to it, Filament Tenancy switching the database connection for instance, does exactly what it does for a page. Tools do not need to know they run over MCP.
The Agent access page shows the workspace's URL (https://acme.test/mcp/acme) and adds the tenant:acme ability to every token it mints there.
A workspace's own key
A workspace can bring its own provider, key and preferred model. Tell the plugin where they come from:
use Packstub\Agents\Ai\WorkspaceCredentials;
AgentsPlugin::make()->credentialsUsing(fn () => Filament::getTenant()
? new WorkspaceCredentials(
provider: $settings->assistant_provider, // 'anthropic' | 'openai'
apiKey: $settings->assistant_api_key,
model: $settings->assistant_model, // a picker key: 'auto' | 'fast' | 'deep'
)
: null);
When the callback returns credentials with a key, the turn runs on that provider with that key (the key is swapped into laravel/ai's config for the request), and the chat is enabled for that workspace even when the platform has no key. Return null for "the platform's provider".
Per-workspace limits
The operator's AI limits resource offers a "One workspace" scope in a panel with tenancy. A workspace row overrides the global one for that tenant; the Assistant switch on it turns the chat off for the workspace. See Budgets and limits.
Database per tenant
By default the package runs its migrations from the vendor directory. In a database-per-tenant app, the tables belong to different databases:
| Table | Where | Why |
|---|---|---|
agent_limits |
central | limits are the operator's |
agent_conversations, agent_conversation_messages, agent_message_feedback |
tenant | one workspace never sees another's chats, and an export or restore carries them along |
So:
// config/packstub-agents.php
'run_migrations' => false,
'limits_connection' => env('AGENT_LIMITS_CONNECTION', 'central'),
php artisan vendor:publish --tag=packstub-agents-migrations
Keep create_agent_limits_table with your central migrations and move create_agent_chat_tables next to your tenant migrations. AgentLimit reads limits_connection, so the operator page works from any panel.
What follows the workspace
| Without tenancy | With tenancy | |
|---|---|---|
| Prompt | "Workspace: {app name}" | "Workspace: {tenant name}" |
| Per-minute rate limit key | central + user |
tenant key + user |
| Turns per day, tokens per month | the whole app | the tenant's database (with database per tenant) |
| Tokens | read / write |
read / write / tenant:{slug} |
| Chat pages, Ask button | always in the panel | only once a tenant is set |