ActionPluginDeriver crashes AI features when an action plugin lacks an instance class
>>> [!note] Migrated issue <!-- Drupal.org comment --> <!-- Migrated from issue #3585335. --> Reported by: [mtift](https://www.drupal.org/user/751908) Related to !1520 >>> <p>[Tracker]<br> <strong>Update Summary: </strong>New bug report &mdash; defensive fix proposed<br> <strong>Short Description: </strong>ActionPluginDeriver crashes the chat explorer when any ECA action plugin lacks a class<br> <strong>Check-in Date: </strong>04/23/2026<br> <em>Metadata is used by the <a href="https://www.drupalstarforge.ai/" title="AI Tracker">AI Tracker.</a> Docs and additional fields <a href="https://www.drupalstarforge.ai/ai-dashboard/docs" title="AI Issue Tracker Documentation">here</a>.</em><br> [/Tracker]</p> <h3 id="summary-problem-motivation">Problem/Motivation</h3> <p><code>ActionPluginDeriver::getDerivativeDefinitions()</code> iterates every ECA action plugin definition and calls <code>createInstance()</code> on each one to discover available function-call tools for AI providers. If any single action plugin definition lacks an instance class, the call throws <code>PluginException</code>, which propagates up and breaks the entire AI chat explorer page (and any other AI feature that enumerates function calls).</p> <p>One bad action plugin anywhere in the site &mdash; including ones contributed by old or deprecated contrib modules &mdash; takes out all AI chat tooling.</p> <p>The trigger in our case was <code>file_entity</code> 8.x-1.14, which declares <code>pathauto_file_update_action</code> via the legacy <code>hook_action_info_alter()</code> without an instance class. That's a real bug in <code>file_entity</code> (we'll file separately), but <code>drupal/ai</code> shouldn't crash because of it &mdash; it should skip the broken definition and continue.</p> <p>Stack trace:</p> <div class="codeblock"> <pre><span style="color: #000000"><span style="color: #0000BB">&lt;?php<br>Drupal</span><span style="color: #007700">\</span><span style="color: #0000BB">Component</span><span style="color: #007700">\</span><span style="color: #0000BB">Plugin</span><span style="color: #007700">\</span><span style="color: #0000BB">Exception</span><span style="color: #007700">\</span><span style="color: #0000BB">PluginException</span><span style="color: #007700">: </span><span style="color: #0000BB">The plugin </span><span style="color: #007700">(</span><span style="color: #0000BB">pathauto_file_update_action</span><span style="color: #007700">) </span><span style="color: #0000BB">did not specify an instance </span><span style="color: #007700">class. </span><span style="color: #0000BB">in Drupal</span><span style="color: #007700">\</span><span style="color: #0000BB">Component</span><span style="color: #007700">\</span><span style="color: #0000BB">Plugin</span><span style="color: #007700">\</span><span style="color: #0000BB">Factory</span><span style="color: #007700">\</span><span style="color: #0000BB">DefaultFactory</span><span style="color: #007700">::</span><span style="color: #0000BB">getPluginClass</span><span style="color: #007700">() (</span><span style="color: #0000BB">line 79 of core</span><span style="color: #007700">/</span><span style="color: #0000BB">lib</span><span style="color: #007700">/</span><span style="color: #0000BB">Drupal</span><span style="color: #007700">/</span><span style="color: #0000BB">Component</span><span style="color: #007700">/</span><span style="color: #0000BB">Plugin</span><span style="color: #007700">/</span><span style="color: #0000BB">Factory</span><span style="color: #007700">/</span><span style="color: #0000BB">DefaultFactory</span><span style="color: #007700">.</span><span style="color: #0000BB">php</span><span style="color: #007700">).<br><br></span><span style="color: #0000BB">Drupal</span><span style="color: #007700">\</span><span style="color: #0000BB">Core</span><span style="color: #007700">\</span><span style="color: #0000BB">Plugin</span><span style="color: #007700">\</span><span style="color: #0000BB">Factory</span><span style="color: #007700">\</span><span style="color: #0000BB">ContainerFactory</span><span style="color: #007700">-&gt;</span><span style="color: #0000BB">createInstance</span><span style="color: #007700">() (</span><span style="color: #0000BB">Line</span><span style="color: #007700">: </span><span style="color: #0000BB">83</span><span style="color: #007700">)<br></span><span style="color: #0000BB">Drupal</span><span style="color: #007700">\</span><span style="color: #0000BB">Component</span><span style="color: #007700">\</span><span style="color: #0000BB">Plugin</span><span style="color: #007700">\</span><span style="color: #0000BB">PluginManagerBase</span><span style="color: #007700">-&gt;</span><span style="color: #0000BB">createInstance</span><span style="color: #007700">() (</span><span style="color: #0000BB">Line</span><span style="color: #007700">: </span><span style="color: #0000BB">178</span><span style="color: #007700">)<br></span><span style="color: #0000BB">Drupal</span><span style="color: #007700">\</span><span style="color: #0000BB">eca</span><span style="color: #007700">\</span><span style="color: #0000BB">PluginManager</span><span style="color: #007700">\</span><span style="color: #0000BB">Action</span><span style="color: #007700">-&gt;</span><span style="color: #0000BB">createInstance</span><span style="color: #007700">() (</span><span style="color: #0000BB">Line</span><span style="color: #007700">: </span><span style="color: #0000BB">67</span><span style="color: #007700">)<br></span><span style="color: #0000BB">Drupal</span><span style="color: #007700">\</span><span style="color: #0000BB">ai</span><span style="color: #007700">\</span><span style="color: #0000BB">Plugin</span><span style="color: #007700">\</span><span style="color: #0000BB">AiFunctionCall</span><span style="color: #007700">\</span><span style="color: #0000BB">Derivative</span><span style="color: #007700">\</span><span style="color: #0000BB">ActionPluginDeriver</span><span style="color: #007700">-&gt;</span><span style="color: #0000BB">getDerivativeDefinitions</span><span style="color: #007700">() (</span><span style="color: #0000BB">Line</span><span style="color: #007700">: </span><span style="color: #0000BB">101</span><span style="color: #007700">)<br></span><span style="color: #0000BB">Drupal</span><span style="color: #007700">\</span><span style="color: #0000BB">Component</span><span style="color: #007700">\</span><span style="color: #0000BB">Plugin</span><span style="color: #007700">\</span><span style="color: #0000BB">Discovery</span><span style="color: #007700">\</span><span style="color: #0000BB">DerivativeDiscoveryDecorator</span><span style="color: #007700">-&gt;</span><span style="color: #0000BB">getDerivatives</span><span style="color: #007700">() (</span><span style="color: #0000BB">Line</span><span style="color: #007700">: </span><span style="color: #0000BB">87</span><span style="color: #007700">)<br></span><span style="color: #0000BB">Drupal</span><span style="color: #007700">\</span><span style="color: #0000BB">Component</span><span style="color: #007700">\</span><span style="color: #0000BB">Plugin</span><span style="color: #007700">\</span><span style="color: #0000BB">Discovery</span><span style="color: #007700">\</span><span style="color: #0000BB">DerivativeDiscoveryDecorator</span><span style="color: #007700">-&gt;</span><span style="color: #0000BB">getDefinitions</span><span style="color: #007700">() (</span><span style="color: #0000BB">Line</span><span style="color: #007700">: </span><span style="color: #0000BB">337</span><span style="color: #007700">)<br></span><span style="color: #0000BB">Drupal</span><span style="color: #007700">\</span><span style="color: #0000BB">Core</span><span style="color: #007700">\</span><span style="color: #0000BB">Plugin</span><span style="color: #007700">\</span><span style="color: #0000BB">DefaultPluginManager</span><span style="color: #007700">-&gt;</span><span style="color: #0000BB">findDefinitions</span><span style="color: #007700">() (</span><span style="color: #0000BB">Line</span><span style="color: #007700">: </span><span style="color: #0000BB">33</span><span style="color: #007700">)<br></span><span style="color: #0000BB">Drupal</span><span style="color: #007700">\</span><span style="color: #0000BB">ai</span><span style="color: #007700">\</span><span style="color: #0000BB">Service</span><span style="color: #007700">\</span><span style="color: #0000BB">FunctionCalling</span><span style="color: #007700">\</span><span style="color: #0000BB">FunctionCallPluginManager</span><span style="color: #007700">-&gt;</span><span style="color: #0000BB">getDefinitions</span><span style="color: #007700">() (</span><span style="color: #0000BB">Line</span><span style="color: #007700">: </span><span style="color: #0000BB">202</span><span style="color: #007700">)<br></span><span style="color: #0000BB">Drupal</span><span style="color: #007700">\</span><span style="color: #0000BB">ai_api_explorer</span><span style="color: #007700">\</span><span style="color: #0000BB">Plugin</span><span style="color: #007700">\</span><span style="color: #0000BB">AiApiExplorer</span><span style="color: #007700">\</span><span style="color: #0000BB">ChatGenerator</span><span style="color: #007700">-&gt;</span><span style="color: #0000BB">buildForm</span><span style="color: #007700">() (</span><span style="color: #0000BB">Line</span><span style="color: #007700">: </span><span style="color: #0000BB">60</span><span style="color: #007700">)<br></span><span style="color: #0000BB">?&gt;</span></span></pre></div> <h4 id="summary-steps-reproduce">Steps to reproduce (required for bugs, but not feature requests)</h4> <p>Environment:</p> <ul> <li>Drupal core 10.x</li> <li><code>drupal/ai</code> 1.3.3</li> <li><code>ai_api_explorer</code> enabled</li> <li>ECA 2.1.x enabled</li> <li>Any module that registers an ECA action plugin definition without a <code>class</code> key (e.g., <code>file_entity</code> 8.x-1.14 with <code>pathauto</code> enabled)</li> </ul> <p>Steps:</p> <ol> <li>Navigate to <code>/admin/config/ai/explorers/chat_generator</code>.</li> <li>Observe a WSOD with the <code>PluginException</code> above.</li> </ol> <h3 id="summary-proposed-resolution">Proposed resolution</h3> <p>Make <code>ActionPluginDeriver::getDerivativeDefinitions()</code> defensive: skip action definitions that lack an instance class so one bad third-party plugin can't take out every AI feature that enumerates function calls.</p> <p>Minimal fix in <code>src/Plugin/AiFunctionCall/Derivative/ActionPluginDeriver.php</code>:</p> <div class="codeblock"> <pre><span style="color: #000000"><span style="color: #0000BB">&lt;?php<br></span><span style="color: #007700">foreach (</span><span style="color: #0000BB">$this</span><span style="color: #007700">-&gt;</span><span style="color: #0000BB">actionManager</span><span style="color: #007700">-&gt;</span><span style="color: #0000BB">getDefinitions</span><span style="color: #007700">() as </span><span style="color: #0000BB">$id </span><span style="color: #007700">=&gt; </span><span style="color: #0000BB">$action_definition</span><span style="color: #007700">) {<br>&nbsp; </span><span style="color: #FF8000">// Skip plugin definitions that lack an instance class. Some legacy<br>&nbsp; // contrib modules register actions via hook_action_info_alter() without<br>&nbsp; // providing a class; those cannot be instantiated.<br>&nbsp; </span><span style="color: #007700">if (empty(</span><span style="color: #0000BB">$action_definition</span><span style="color: #007700">[</span><span style="color: #DD0000">'class'</span><span style="color: #007700">])) {<br>&nbsp;&nbsp;&nbsp; continue;<br>&nbsp; }<br>&nbsp; if (</span><span style="color: #0000BB">in_array</span><span style="color: #007700">(</span><span style="color: #0000BB">$action_definition</span><span style="color: #007700">[</span><span style="color: #DD0000">'type'</span><span style="color: #007700">], </span><span style="color: #0000BB">$supported_action_types</span><span style="color: #007700">, </span><span style="color: #0000BB">TRUE</span><span style="color: #007700">) || </span><span style="color: #0000BB">$this</span><span style="color: #007700">-&gt;</span><span style="color: #0000BB">entityTypeManager</span><span style="color: #007700">-&gt;</span><span style="color: #0000BB">getDefinition</span><span style="color: #007700">(</span><span style="color: #0000BB">$action_definition</span><span style="color: #007700">[</span><span style="color: #DD0000">'type'</span><span style="color: #007700">], </span><span style="color: #0000BB">FALSE</span><span style="color: #007700">)) {<br>&nbsp;&nbsp;&nbsp; </span><span style="color: #FF8000">// ... existing logic, including the createInstance() call ...<br>&nbsp; </span><span style="color: #007700">}<br>}<br></span><span style="color: #0000BB">?&gt;</span></span></pre></div> <p>A <code>try/catch</code> around <code>createInstance()</code> would be a defense-in-depth alternative if maintainers prefer to also tolerate other instantiation failures.</p> <p>The same defensive pattern likely belongs in any other deriver in the module that enumerates third-party plugins it doesn't own.</p> <h3 id="summary-remaining-tasks">Remaining tasks</h3> <ul> <li>Decide on guard style: pre-check on <code>['class']</code>, <code>try/catch</code>, or both</li> <li>Audit other derivers in <code>src/Plugin/AiFunctionCall/Derivative/</code> for the same fragility</li> <li>Patch + test against a site with a deliberately broken action plugin</li> <li>Reviews</li> </ul> <h3>Related issues</h3> <ul> <li><span class="drupalorg-gitlab-issue-link project-issue-status-info project-issue-status-8"><a href="https://www.drupal.org/project/ai/issues/3585328" title="Status: Needs review">#3585328: Undefined array key "#type" warnings break AJAX in AbstractModelFormBase::updateModelForm()</a></span> &mdash; separate <code>drupal/ai</code> bug we filed in the same session, surfaced while configuring a Vertex chat model</li> </ul> <h3>Optional: Other details as applicable (e.g., User interface changes, API changes, Data model changes)</h3> <p>No UI, API, or data model changes. The fix makes the deriver resilient to malformed third-party plugin definitions.</p> <h3 id="summary-ai-usage">AI usage (if applicable)</h3> <p>[x] AI Assisted Issue<br><br> This issue was generated with AI assistance, but was reviewed and refined by the creator.</p> <p>[ ] AI Assisted Code<br><br> [ ] AI Generated Code<br><br> [ ] Vibe Coded</p>
issue