Canvas AI: Create the canvas_dev_ai module with a mock AI controller
## Summary Create a new module `canvas_dev_ai` at `modules/canvas_dev_ai/` (sibling to `modules/canvas_ai/`). It adds a `js_settings_alter` hook that exposes a dev flag to `drupalSettings`, and a `CanvasDevAiBuilder` controller with a single `render()` action that returns a fixed mocked JSON response. It adds **one** new route (`/admin/api/canvas/ai-dev`) and **reuses the existing `canvas_ai.csrf_token` endpoint** for CSRF. No agent, no tools, no progress polling. This stands up the module and its request path; the real hop loop replaces the mock in a later issue. ## Why the module exists The rewrite changes the AI controller, adds new tools, and replaces the agent loop. `canvas_dev_ai` is an experimental, opt-in module that holds this work so it can be built and released without altering the live `canvas_ai` editor. It wires to no live agent; installing it swaps the AI editor for the dev controller, and uninstalling it restores the old flow. The work graduates into `canvas_ai` in one controlled MR later. - Roadmap: https://git.drupalcode.org/project/canvas/-/work_items/3591777 - POC (consumer side): https://git.drupalcode.org/project/canvas/-/merge_requests/1214 ## Files to create ``` modules/canvas_dev_ai/ canvas_dev_ai.info.yml canvas_dev_ai.routing.yml src/ Controller/CanvasDevAiBuilder.php Hook/CanvasDevAiHooks.php ``` ### `canvas_dev_ai.info.yml` Match the active feature-flag dev-module precedent `canvas_dev_cd` (`modules/canvas_dev_cd/canvas_dev_cd.info.yml`): a plain module whose description states the consequence. Do **not** set `lifecycle` or `hidden` — in Canvas those mark *retired* modules only (`canvas_dev_er`, `canvas_dev_translation` use `lifecycle: deprecated` + `hidden: true`). ```yaml name: Drupal Canvas Dev AI type: module description: 'Enables the experimental Canvas AI client-side orchestration loop - feature flag until the revamped Canvas AI is fully stable. Replaces the AI chat backend and UI while installed.' package: Drupal Canvas core_version_requirement: ^11.3 dependencies: - canvas:canvas - canvas:canvas_ai ``` ### `src/Hook/CanvasDevAiHooks.php` Add a hook class with a `#[Hook('js_settings_alter')]` method `jsSettingsAlter(array &$settings)`. Mirror `Drupal\canvas_ai\Hook\CanvasAiHooks::jsSettingsAlter()` (`modules/canvas_ai/src/Hook/CanvasAiHooks.php:158`): guard on `!empty($settings['canvas']['aiExtensionAvailable'])`, then set: ```php $settings['canvas']['aiDevMode'] = TRUE; ``` This is the flag the dev wizard (`AiWizardDev.tsx`, separate issue) reads to swap itself in and post to `/admin/api/canvas/ai-dev`. Mark the class `@internal`. ### `canvas_dev_ai.routing.yml` **One** route only — the chat endpoint. No token route. ```yaml canvas_dev_ai.builder: path: '/admin/api/canvas/ai-dev' defaults: _controller: '\Drupal\canvas_dev_ai\Controller\CanvasDevAiBuilder::render' requirements: _permission: 'use Drupal Canvas AI' ``` Permission `use Drupal Canvas AI` is inherited from the `canvas_ai` dependency — do not define a new permission. No token route: the frontend already fetches its CSRF token from `canvas_ai`'s `/admin/api/canvas/token` and reuses it for every POST, so the dev controller just validates against the same seed (see the controller below). ### `src/Controller/CanvasDevAiBuilder.php` A `final` `@internal` controller extending `ControllerBase`. Inject only `csrf_token` (`Drupal\Core\Access\CsrfTokenGenerator`). - `render(Request $request): JsonResponse` — validate the `X-CSRF-Token` header against the seed `'canvas_ai.canvas_builder'` (the same seed `canvas_ai`'s token endpoint issues; throw `AccessDeniedHttpException` on failure, as `CanvasBuilder::render()` does at lines 75–78), then return the fixed mock. Add a class-level `@todo` noting the mock is temporary and will be replaced by the real hop loop (mirroring how `canvas_ai` marks its gates, `CanvasController.php:127`; link the graduation/controller work item once it exists). ```json { "status": true, "should_continue": false, "message": "This is a mocked response from the Canvas Dev AI controller.", "progress": "" } ``` No `getCsrfToken()` method — the frontend fetches its token from the existing `/admin/api/canvas/token`. The mock matches the shape `CanvasBuilder::render()` returns (`status`, `message`, `should_continue`, `progress`), so it is the frozen frontend contract for `AiWizardDev.tsx`. ## Explicitly excluded (do NOT copy from `canvas_ai`) - A token endpoint — reuse `canvas_ai.csrf_token`; validate against seed `'canvas_ai.canvas_builder'`. - Agent invocation, `main_agent` config, `getToolResults()`, structured-output merge. - Progress/polling: no `getProgressItems()`, no `ai_agents.agent_status_poller`, no progress endpoint. - File upload handling, chat-history reconstruction, `CanvasAiPageBuilderHelper`/`CanvasAiTempStore`/`CanvasAiChatHelper` — none are needed for the mock. - Config entities and agents: none. Dev agents need new `canvas_dev_ai_*` config IDs to avoid `PreExistingConfigException`; they are a separate issue. ## Tests `canvas_dev_ai` ships no tests of its own (same as `canvas_dev_cd`). The coverage lives in `canvas_ai`'s suite as one kernel test, `modules/canvas_ai/tests/src/Kernel/CanvasAiDevControllerTest.php` extending `CanvasKernelTestBase`, that enables `canvas_dev_ai` and checks two things: 1. **Flag follows install state.** With `canvas_dev_ai` installed, `canvas.aiDevMode` is exposed to `drupalSettings`; after uninstalling it, the key is gone. 2. **Controller returns the mock.** `CanvasDevAiBuilder::render()` returns the exact mock body above. Keep it direct — no extra cases. This test is temporary; put a `@todo Remove in <work-item URL>` at the top of the class (mirroring `ApiLayoutControllerGetTest.php:710`) matching the `@todo` on the controller, so it is deleted/replaced when the real hop loop lands. --- _Issue generated with AI assistance._
issue