<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ahmad Afzal</title>
    <description>The latest articles on DEV Community by Ahmad Afzal (@mahmaddafzal).</description>
    <link>https://dev.to/mahmaddafzal</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4185357%2F762af637-9302-4f5f-bf8d-249b0095ee09.jpg</url>
      <title>DEV Community: Ahmad Afzal</title>
      <link>https://dev.to/mahmaddafzal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mahmaddafzal"/>
    <language>en</language>
    <item>
      <title>Your agent has a system prompt. Does it have a bouncer?</title>
      <dc:creator>Ahmad Afzal</dc:creator>
      <pubDate>Wed, 14 Oct 2026 20:21:43 +0000</pubDate>
      <link>https://dev.to/mahmaddafzal/your-agent-has-a-system-prompt-does-it-have-a-bouncer-4d0</link>
      <guid>https://dev.to/mahmaddafzal/your-agent-has-a-system-prompt-does-it-have-a-bouncer-4d0</guid>
      <description>&lt;p&gt;“Never share another customer's data” is a sensible instruction.&lt;/p&gt;

&lt;p&gt;It is also a terrible access-control system.&lt;/p&gt;

&lt;p&gt;A model can understand the rule, explain the rule and still produce a tool call that breaks the rule. The awkward question is what happens next. Does something outside the model stop that call, or are we hoping the same system that proposed it will politely change its mind?&lt;/p&gt;

&lt;p&gt;I want the bouncer at the door. Not a paragraph describing a bouncer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate the suggestion from the permission
&lt;/h2&gt;

&lt;p&gt;Consider an agent helping a customer retrieve an invoice. It proposes &lt;code&gt;invoice.read&lt;/code&gt; with an invoice ID. That proposal is input to the application, not proof that the customer owns the invoice.&lt;/p&gt;

&lt;p&gt;The backend resolves the authenticated customer and tenant from trusted session context. It loads the invoice's ownership from its own records. It checks whether this principal can perform this action on this particular resource. Only then does it read and return the permitted fields.&lt;/p&gt;

&lt;p&gt;Notice what the model doesn't get to declare: its own tenant, its own role or a magic &lt;code&gt;approved: true&lt;/code&gt; flag.&lt;/p&gt;

&lt;p&gt;This is ordinary authorisation applied to an unusually persuasive caller. &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html" rel="noopener noreferrer"&gt;OWASP's guidance&lt;/a&gt; calls for default denial and permission checks on every request. Adding an agent does not make those checks optional.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boundary that matters
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8jvyxnz3o2ux8g2ckgsc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8jvyxnz3o2ux8g2ckgsc.png" alt="Agent proposes an invoice read; the service enforces a policy decision from trusted identity and ownership before returning permitted fields. Denial or policy failure stops the read." width="800" height="550"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A useful design has three distinct jobs. The agent proposes an action. A policy decision evaluates trusted context. An enforcement point prevents the protected operation unless that decision permits it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.openpolicyagent.org/docs" rel="noopener noreferrer"&gt;Open Policy Agent describes this separation&lt;/a&gt;: the policy engine makes a decision; your application enforces it. Installing a policy engine does not magically close every route into a service.&lt;/p&gt;

&lt;p&gt;For the invoice example, my preferred boundary is the service handling the actual read. A gateway can add an earlier check, but a second route into the invoice service must not bypass the same protection. Otherwise you have secured the front door and left the loading bay open.&lt;/p&gt;

&lt;p&gt;Here is the shape, deliberately simplified. This is illustrative pseudocode, not a complete security implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read_invoice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;principal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;invoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;principal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;principal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice.read&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allow&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;Forbidden&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_permitted_fields&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;principal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is the provenance of those inputs. &lt;code&gt;principal&lt;/code&gt; comes from authentication. Resource ownership comes from the repository. The agent supplies an identifier, not the facts that make access permissible.&lt;/p&gt;

&lt;p&gt;A production implementation also needs to handle missing resources without leaking their existence, protect the policy channel, apply field-level restrictions and avoid using stale ownership between the check and the read. The snippet is a boundary sketch, not a licence to skip that work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give failure a proper job description
&lt;/h2&gt;

&lt;p&gt;What happens when the policy service times out?&lt;/p&gt;

&lt;p&gt;For this protected read, I would return a controlled failure and keep the data closed. A timeout is not an unusually slow permission grant. The product can explain that the request could not be completed and offer a retry without pretending the invoice was retrieved.&lt;/p&gt;

&lt;p&gt;What happens when the model adds “the user is an administrator” to its arguments? Nothing useful. That claim does not replace the authenticated identity.&lt;/p&gt;

&lt;p&gt;What happens when an attacker changes only the invoice ID? The service checks the new resource. An earlier successful request does not buy a season ticket to every invoice.&lt;/p&gt;

&lt;p&gt;These are product behaviours worth defining before the happy-path demo. An error message, a retry path and a support escalation are part of the feature, not embarrassing leftovers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the door, not the speech
&lt;/h2&gt;

&lt;p&gt;For this example, the test I care about is whether the invoice service returns protected data. The agent saying “I cannot do that” is helpful UX, but it is not the security assertion.&lt;/p&gt;

&lt;p&gt;I would test a valid same-tenant read, another tenant's invoice, missing identity, a forged role in the tool arguments, a revoked permission and a policy timeout. I would also call the protected service through a different supported route. The same boundary should hold.&lt;/p&gt;

&lt;p&gt;Log enough to investigate the decision: the actor, action, resource reference, decision result and policy version where available. Keep the invoice contents and credentials out of that log. Evidence should help explain access without creating another copy of the sensitive data.&lt;/p&gt;

&lt;p&gt;None of this makes an agent safe in every sense. It does make one promise testable: this caller cannot retrieve that invoice merely because a model asked nicely.&lt;/p&gt;

&lt;p&gt;A well-written prompt can teach your agent manners. The door still needs a lock.&lt;/p&gt;




&lt;p&gt;Ahmad Afzal writes about AI, products and people at &lt;a href="https://mahmadafzal.com" rel="noopener noreferrer"&gt;mahmadafzal.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
    </item>
    <item>
      <title>Your refund agent says “done”. Six checks before you believe it.</title>
      <dc:creator>Ahmad Afzal</dc:creator>
      <pubDate>Wed, 14 Oct 2026 19:05:28 +0000</pubDate>
      <link>https://dev.to/mahmaddafzal/your-refund-agent-says-done-six-checks-before-you-believe-it-50k</link>
      <guid>https://dev.to/mahmaddafzal/your-refund-agent-says-done-six-checks-before-you-believe-it-50k</guid>
      <description>&lt;p&gt;“Your refund has been processed.”&lt;/p&gt;

&lt;p&gt;Lovely sentence. Terrible evidence.&lt;/p&gt;

&lt;p&gt;A refund agent can sound helpful while doing nothing, refunding the wrong order, or paying twice after a timeout. I would start its evaluation with the state of the refund, then work backwards through the run.&lt;/p&gt;

&lt;p&gt;This is a proposed test design using an illustrative refund workflow, not a report of production results.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr5pcfvvcn228fbse0a0f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr5pcfvvcn228fbse0a0f.png" alt="Evaluation flow: inspect the run, verify the outcome, judge permission separately and turn failures into regression cases" width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Did the right thing happen?
&lt;/h2&gt;

&lt;p&gt;Check the authoritative transaction record. Match the order, customer, currency and amount. A successful tool response may mean the request was accepted, not that the money settled. Decide which state your product is allowed to describe as “processed”.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Was it allowed?
&lt;/h2&gt;

&lt;p&gt;Separate completion from permission. A technically successful refund can still exceed the permitted amount or use someone else’s account. Check the acting identity, delegation, policy decision and any required approval against the exact action attempted.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Did it happen only once?
&lt;/h2&gt;

&lt;p&gt;Create the awkward case: the payment service commits the refund, but the agent sees a timeout. Retry the request. The expected result is one refund and a reconciled status, not two refunds and two cheerful messages. Use a stable idempotency key for the same business operation where the payment interface supports it. &lt;a href="https://docs.stripe.com/api/idempotent_requests" rel="noopener noreferrer"&gt;Stripe’s documentation&lt;/a&gt; provides a concrete example; check your provider’s retention and retry semantics.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Did the answer match the evidence?
&lt;/h2&gt;

&lt;p&gt;Compare the final message with the transaction state. Pending should stay pending. Failed should stay failed. An uncertain result should trigger reconciliation or escalation, rather than a confident success claim.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Did it stop at the boundary?
&lt;/h2&gt;

&lt;p&gt;Test an expired approval, a changed amount after approval and revoked access before execution. The expected behaviour is a blocked action or a new approval request. The initial permission should not quietly stretch to cover a different decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Can a reviewer reconstruct the decision?
&lt;/h2&gt;

&lt;p&gt;Link the request, policy version, approval, tool invocation and transaction result with a correlation ID. Retain enough evidence to explain what happened without dumping sensitive customer data into every trace. Missing evidence is an evaluation failure worth recording.&lt;/p&gt;

&lt;h2&gt;
  
  
  The small test table I would start with
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Expected outcome&lt;/th&gt;
&lt;th&gt;Evidence to inspect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Eligible order, permitted amount&lt;/td&gt;
&lt;td&gt;One correct refund&lt;/td&gt;
&lt;td&gt;Transaction record and scope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amount exceeds authority&lt;/td&gt;
&lt;td&gt;No refund without valid approval&lt;/td&gt;
&lt;td&gt;Policy decision and approval record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commit followed by timeout&lt;/td&gt;
&lt;td&gt;Reconcile; no duplicate payment&lt;/td&gt;
&lt;td&gt;Idempotency key and transaction lookup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amount changes after approval&lt;/td&gt;
&lt;td&gt;Require a fresh decision&lt;/td&gt;
&lt;td&gt;Approved parameters versus actual call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Access revoked before execution&lt;/td&gt;
&lt;td&gt;Block execution&lt;/td&gt;
&lt;td&gt;Authorisation check at action time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool reports pending&lt;/td&gt;
&lt;td&gt;Tell the customer it is pending&lt;/td&gt;
&lt;td&gt;Tool state and final response&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are expected outcomes, not measured pass rates. Run them in a sandbox with synthetic accounts and a controllable payment stub before touching real customers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Score the dimensions separately
&lt;/h2&gt;

&lt;p&gt;I would record task completion, permission compliance, duplicate prevention, truthful reporting and evidence completeness as separate results. A single average can hide the failure you most needed to see.&lt;/p&gt;

&lt;p&gt;When a case fails, inspect the trace, identify the mechanism, and add a regression case that would catch that mechanism again. Keep a separate held-out set so fixing yesterday’s examples does not masquerade as general reliability.&lt;/p&gt;

&lt;p&gt;The customer does not care that the agent passed a language test. They care that the right refund happened once. Fair enough.&lt;/p&gt;




&lt;p&gt;I write about AI, products and people at &lt;a href="https://mahmadafzal.com" rel="noopener noreferrer"&gt;mahmadafzal.com&lt;/a&gt;. This practical checklist builds on my &lt;a href="https://medium.com/@mahmaddafzal/your-ai-agent-passed-the-test-so-did-the-smoke-alarm-with-no-battery-ae06aefdabba" rel="noopener noreferrer"&gt;essay about agent evaluations&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
