Restrict Topic guardrail silently bypassed due to case-sensitive topic matching
>>> [!note] Migrated issue
<!-- Drupal.org comment -->
<!-- Migrated from issue #3579079. -->
Reported by: [abhisekmazumdar](https://www.drupal.org/user/3557964)
Related to !1322
>>>
<p>[Tracker]<br>
<strong>Update Summary: </strong>[One-line status update for stakeholders]<br>
<strong>Short Description: </strong>[One-line issue summary for stakeholders]<br>
<strong>Check-in Date: </strong>MM/DD/YYYY<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>Problem</h3>
<p>This is something I faced while I was testing: <span class="drupalorg-gitlab-issue-link drupalorg-gitlab-link-wrapper"><a href="https://git.drupalcode.org/project/ai_initiative/-/work_items/3577506" class="drupalorg-gitlab-link">https://git.drupalcode.org/project/ai_initiative/-/work_items/3577506</a></span></p>
<p>In <code>RestrictToTopic::processInput()</code>, the topics returned by the LLM classification call are matched against configured topics using <code>\in_array()</code>. This comparison is case-sensitive by default.</p>
<p>The LLM response is non-deterministic. It may return <code>"Legal Advice"</code>, <code>"legal advice"</code>, or <code>"LEGAL ADVICE"</code> for the same prompt. If the casing returned by the LLM does not exactly match the casing stored in config, neither <code>$invalid_topics_found</code> nor <code>$valid_topics_found</code> will be populated and the guardrail will silently pass when it should block.</p>
<h3>Steps to reproduce</h3>
<ol>
<li>Configure a guardrail using the <code>restrict_to_topic</code> plugin with <code>invalid_topics</code> containing <code>legal advice</code> (lowercase, as shipped in the <code>ai_recipe_guardrails_prompt_safety</code> recipe). Just like <span class="drupalorg-gitlab-issue-link drupalorg-gitlab-link-wrapper"><a href="https://git.drupalcode.org/project/ai_initiative/-/work_items/3577506" class="drupalorg-gitlab-link">https://git.drupalcode.org/project/ai_initiative/-/work_items/3577506</a></span></li>
<li>Send a prompt asking for legal advice.</li>
<li>Observe that on some runs the LLM returns <code>"Legal Advice"</code> (title case) and the guardrail passes incorrectly.</li>
</ol>
<h3>Expected behaviour</h3>
<p>Topic matching should be case-insensitive. A topic returned as <code>"Legal Advice"</code> should match a configured topic of <code>"legal advice"</code>.</p>
<h3>Proposed fix</h3>
<p>Normalise both the configured topics and the LLM-returned topics to lowercase before comparison using <code>mb_strtolower()</code>, which ensures correct handling of multibyte and non-ASCII characters in topic names.</p>
<h3 id="summary-ai-usage">AI usage (if applicable)</h3>
<p>[ x ] AI Assisted Issue<br>
This issue was generated with AI assistance, but was reviewed and refined by the creator.</p>
<p>[ ] AI Assisted Code<br>
This code was mainly generated by a human, with AI autocompleting or parts AI generated, but under full human supervision.</p>
<p>[ ] AI Generated Code<br>
This code was mainly generated by an AI with human guidance, and reviewed, tested, and refined by a human.</p>
<p>[ ] Vibe Coded<br>
This code was generated by an AI and has only been functionally tested.</p>
issue