Features

Markdown

AI tool approvals & audit

From externa-core 1.4.0, the in-app assistant and the MCP server share one permission-aware tool registry (#202). Tools that write or delete data are denied by default, need a human approval when enabled, and every tool decision lands in a redacted audit log (#203).

Related

Agent and per-action permissions: AI assistant model. Chat UI: AI chat. Env keys: AI configuration.

Tool kinds

Each registered tool has a kind (AiToolKindEnum):

KindTools
readDescribeFieldTypes, ExtractPdfText, GetImportJobStatus, QueryCollectionItems, ExportCollection, SearchSimilarCollectionItems, QueryActivityLogs
writeImportCollectionCsv, ImportRemoteJson
destructiveManageCollections, ManageCollectionItems, ManageAiSyncSources, ManageFiles, ManageUsers, ManageRoles, ManageGroups, RollbackLastAiTurn

write and destructive tools are mutating. The Manage* tools mix read and write actions through their action argument (for example list / get vs create / delete).

Write gate

A mutating tool may run only when both are true:

  1. Kill switch on: AI_WRITE_TOOLS=true (also accepted as AI_TOOLS_WRITE_ENABLED=true). Default false.
  2. Tool on the allowlist: its class name is in AI_TOOLS_WRITE_ALLOW (CSV) or selected under Settings → AI write tools (/settings/ai-tools, project setting ai.write_tools, needs can-manage-project-settings). The two lists are merged. An empty allowlist denies every mutating tool.
AI_WRITE_TOOLS=true
AI_TOOLS_WRITE_ALLOW=ManageCollectionItems,ImportCollectionCsv

Three more keys filter tools for every kind, before the write gate:

EnvDefaultEffect
AI_TOOLS_ENABLEDtruefalse turns off every AI tool in chat and MCP.
AI_TOOLS_ALLOWemptyWhen set, only these tool names are available at all.
AI_TOOLS_DENYemptyThese tool names are never available (wins over ALLOW).

The registry then checks the actor's permissions and each tool re-checks action-level permissions, as before.

Chat: read actions keep working

With the write gate closed (the default), chat still lists the Manage* tools as a read-only copy: list, get, search and other read actions run, while every write or destructive action is refused with an English machine message the model relays (#439). This keeps the 1.3.0 behaviour for reads. Pure write tools (ImportCollectionCsv, ImportRemoteJson) and RollbackLastAiTurn are not offered in chat until they are allowlisted.

When a tool is allowlisted and the kill switch is on, its write and destructive actions pause for the user's approval in the chat (laravel/ai Approvable) before they run. Examples of write actions: ManageCollectionItems create / update / publish / restore / bulk_update; ManageFiles create / rename / move / tag / save_attachment. Destructive: delete, force_delete, bulk_delete.

The assistant's approvals Agent Skill explains pending, rejected, and abandoned approvals to the user.

MCP: pending approvals

On the MCP surface, an allowed mutating call does not run. The executor stores a PendingToolCall and returns pending_approval to the MCP client.

ItemDetail
InboxSettings → AI tool approvals (GET /settings/pending-tool-calls, permission can-use-ai)
ActionsPOST /settings/pending-tool-calls/{id}/approve, …/reject
ApproverMust also pass the tool's permission check for those arguments, otherwise the approval is refused
ExpiryAI_TOOLS_APPROVAL_TTL minutes (default 15); overdue rows become expired and never run
Shown fieldsTool, redacted arguments, requester, expiry

Approving runs the call once and records executed or failed. MCP write tools are listed only when AI_MCP_WRITE_ENABLED and the key's mcp:write scope allow it — see MCP server — Write tools.

Audit log

Every registry decision for a signed-in actor (allowed, denied, pending, executed) is written to the activity log with log_name=ai and event=ai_tool, including the tool, the decision, the status, and the surface (chat or mcp). Filter it in Activity log by the ai_tool event.

Arguments are redacted before they are logged or shown in the approval inbox. AI_TOOLS_REDACT_KEYS (CSV) sets the redacted keys. Default:

auth_bearer,auth_header,authorization,bearer,password,token,api_key,apikey,secret,client_secret,email,phone,telephone,ssn,social_security_number,credit_card,card_number,cvv

Retention: ai:prune-tool-audit

php artisan ai:prune-tool-audit

Scheduled daily in routes/console.php. It:

  1. expires overdue pending approvals;
  2. deletes ai_tool activity rows older than AI_TOOLS_AUDIT_RETENTION_DAYS (default 90);
  3. deletes resolved pending_tool_calls (rejected, expired, executed, failed) older than the same window.

The scheduler must be running — see Queues & scheduler.

Environment summary

EnvDefaultConfig
AI_TOOLS_ENABLEDtrueai.tools.enabled
AI_WRITE_TOOLSfalseai.tools.write_enabled
AI_TOOLS_WRITE_ENABLEDfalseFallback for AI_WRITE_TOOLS
AI_TOOLS_WRITE_ALLOWemptyai.tools.write_allow
AI_TOOLS_ALLOWemptyai.tools.allow
AI_TOOLS_DENYemptyai.tools.deny
AI_TOOLS_APPROVAL_TTL15ai.tools.approval_ttl_minutes
AI_TOOLS_AUDIT_RETENTION_DAYS90ai.tools.audit_retention_days
AI_TOOLS_REDACT_KEYSsee aboveai.tools.redact_keys

Source map

ConcernLocation (externa-core)
Registryapp/Services/Ai/AiToolRegistryService.php
Executorapp/Services/Ai/AiToolExecutorService.php
Write gateapp/Services/Ai/AiToolWriteGateService.php
Approvalsapp/Services/Ai/AiToolApprovalService.php, app/Models/PendingToolCall.php
Chat approvalapp/Ai/Concerns/RequiresDestructiveApprovalTrait.php
Inbox UIPendingToolCallController, resources/js/pages/admin/pending-tool-calls/index.tsx
Settings UIAiToolSettingsController, resources/js/pages/settings/ai-tools.tsx
Prune commandapp/Console/Commands/Ai/PruneAiToolAuditCommand.php
Previous
AI chat