# Extending Externa

Source: https://docs.qiick.io/externa/extending

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](https://docs.qiick.io/externa/permissions). Field catalogue: [Field types](https://docs.qiick.io/externa/field-types). Automation: [Automation without Flows](https://docs.qiick.io/externa/automation-without-flows). Contrib norms: [Contributing](https://docs.qiick.io/externa/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

```bash
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

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

### 5. Frontend `useCan`

```tsx
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:

| Piece                    | Typical location                                                                                                                     |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
| Normalize inbound values | `CollectionItemDataNormalizer`                                                                                                       |
| Build Laravel rules      | `CollectionItemDataRuleBuilder` / `FieldValidationRuleEvaluator`                                                                     |
| Field settings schema    | `ValidatesCollectionFieldSettings` + Store/Update field Form Requests (`Rule::enum(FieldTypeEnum::class)` already constrains `type`) |

### 3. UI

| Piece                 | Location                                                                                |
| --------------------- | --------------------------------------------------------------------------------------- |
| Type option catalogue | `resources/js/lib/collection-field-types.ts`                                            |
| Field settings panels | `resources/js/components/collections/field-settings/…`                                  |
| Item form input       | `resources/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](https://docs.qiick.io/externa/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`

```php
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
