Contributing

Extending Externa

Three common extension paths touch the same sources of truth: enums, sync/seed, HTTP middleware or Form Requests, React useCan, and AI tools. Follow the checklists so UI, API, and assistant stay aligned.

Related

Permissions model: Effective permissions. Field catalogue: Field types. Contrib norms: Contributing.

Add a permission

1. Enum

Add a case to app/Enums/PermissionEnum.php with a kebab-case string value, e.g. CanExportReports = 'can-export-reports'.

2. Sync

php artisan permissions:sync

This upserts the Spatie permissions row and regenerates resources/js/enums/permission-enum.ts. Use --prune only when you intentionally remove enum cases.

3. Roles

  • Update RoleSeeder (or assign in admin UI) so admin / custom roles receive the new permission.
  • super-admin already resolves to all PermissionEnum::values() at runtime via EffectivePermissionResolver.
  • reader only gets can-show-* names at seed time — new show permissions matching that prefix are included when you re-seed or assign manually.

4. Middleware / Form Requests

SurfacePattern
Admin-style routes->middleware('permission:'.PermissionEnum::CanExportReports->value)
Controllers / bulk$this->authorizePermission(PermissionEnum::…->value)
Files-like mappingExtend EnsureCanManageFiles match arms if it belongs to the file manager

5. Frontend useCan

import { PermissionEnum } from '@/enums/permission-enum';
import { useCan } from '@/hooks/use-can';

const { can } = useCan();
if (!can(PermissionEnum.CanExportReports)) {
  return null;
}

Shared props already expose auth.permissions from HandleInertiaRequests.

6. AI tools

If the assistant should perform the action:

  1. Call $this->requirePermission(PermissionEnum::CanExportReports) inside the tool (ChecksAiPermissions).
  2. Register/include the tool in App\Ai\Agents\AppAssistant::tools() when the user has the relevant permission(s).
  3. Mention the capability in instructions() / capabilityLines() so the model knows the tool exists.

7. Tests

  • Feature: denied without permission, allowed with it (see tests/Feature/Authorization/, Admin suites).
  • AI: matrix/smoke tests under tests/Feature/Ai/ if a tool is involved.

Add a field type

1. Enum

Add a case to app/Enums/FieldTypeEnum.php (string value). Update helpers if needed:

  • isArrayStorage()
  • isRelationType() / isMultipleRelationType()

2. Normalizer & validation rules

Wire storage and validation through Collections services:

PieceTypical location
Normalize inbound valuesCollectionItemDataNormalizer
Build Laravel rulesCollectionItemDataRuleBuilder / FieldValidationRuleEvaluator
Field settings schemaValidatesCollectionFieldSettings + Store/Update field Form Requests (Rule::enum(FieldTypeEnum::class) already constrains type)

3. UI

PieceLocation
Type option catalogueresources/js/lib/collection-field-types.ts
Field settings panelsresources/js/components/collections/field-settings/…
Item form inputresources/js/components/collections/dynamic-item-fields.tsx (+ rich inputs if needed)

Keep locale-aware settings (display_name, notes) consistent with project content locales (see Configuration).

4. AI instructions

AppAssistant::instructions() embeds FieldTypeEnum::values() and tells the model to pass enum strings to create_field. Update tool descriptions in ManageCollections if the new type needs special settings JSON.

maxSteps() already scales with count(FieldTypeEnum::cases()).

5. Tests

  • Normalizer/rule unit or feature coverage under tests/Feature/Collections/.
  • Optional AI tool test that creates a field of the new type.

Add an AI tool

1. Tool class

Create app/Ai/Tools/YourTool.php implementing the Laravel AI Tool contract (mirror an existing tool such as QueryCollectionItems or ManageFiles).

2. ChecksAiPermissions

use App\Ai\Concerns\ChecksAiPermissions;

class YourTool /* … */ {
    use ChecksAiPermissions;

    public function handle(/* … */): mixed
    {
        if ($error = $this->requirePermission(PermissionEnum::CanShowSomething)) {
            return $error; // English machine-error string (model translates for the user)
        }
        // …
    }
}

Return structured JSON with "ok": true on success so the agent instructions (“never invent success”) stay honest.

3. Register on AppAssistant

In tools():

  1. Gate with EffectivePermissionResolver (hasPermission / canAny).
  2. $tools[] = new YourTool;
  3. Extend capabilityLines() / instructions so the model knows when to call it.

4. HTTP (optional)

Only add routes if the UI needs a non-chat endpoint (status polling, uploads). Chat itself stays on POST /ai/chat.

5. Tests

Add Pest coverage under tests/Feature/Ai/:

  • Permission denied vs allowed (AiPermissionMatrixTest patterns).
  • Happy-path tool action against the DB (AiToolActionsDbTest patterns).
  • Use helper grantAiPermissions($user, […]) from tests/Pest.php.

6. Config flags

If the tool is experimental, gate it behind config/ai.php (same pattern as embeddings.enabled / mcp.enabled) and document the env key.


After extending

  1. composer lint / npm run lint:check / npm run types:check
  2. composer test or focused Pest filters
  3. Update Markdoc if routes or config contracts changed
  4. graphify update . from the Externa workspace root when architecture changed
Previous
How to contribute