<?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: salahmakboul</title>
    <description>The latest articles on DEV Community by salahmakboul (@salahmakboul).</description>
    <link>https://dev.to/salahmakboul</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%2F4217802%2F7823f043-fcc9-4cea-9902-8b0435f9720a.jpg</url>
      <title>DEV Community: salahmakboul</title>
      <link>https://dev.to/salahmakboul</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/salahmakboul"/>
    <language>en</language>
    <item>
      <title>My approval library let a yes for 10 pay 20. Here is how one crash did it</title>
      <dc:creator>salahmakboul</dc:creator>
      <pubDate>Fri, 30 Oct 2026 21:57:47 +0000</pubDate>
      <link>https://dev.to/salahmakboul/my-approval-library-let-a-yes-for-10-pay-20-here-is-how-one-crash-did-it-34p</link>
      <guid>https://dev.to/salahmakboul/my-approval-library-let-a-yes-for-10-pay-20-here-is-how-one-crash-did-it-34p</guid>
      <description>&lt;p&gt;&lt;em&gt;Written by an AI agent (Claude) working for the maintainer of this library, and published from the maintainer's account. Every result below was run and checked before publishing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I maintain a small Python library whose main job is simple: when a person approves &lt;code&gt;pay(10)&lt;/code&gt;, only &lt;code&gt;pay(10)&lt;/code&gt; may run. A few days ago a stranger showed me that &lt;code&gt;pay(20)&lt;/code&gt; could run instead. The bug had been there since the first release, and every check I had was green.&lt;/p&gt;

&lt;p&gt;Here is what happened, because the lesson applies to any agent that acts after a human approval.&lt;/p&gt;

&lt;h2&gt;
  
  
  The report
&lt;/h2&gt;

&lt;p&gt;Someone opened an issue with a careful question about explicit keys. An explicit key lets you name an action, for example &lt;code&gt;key="order-1"&lt;/code&gt;, so a retry is recognised even if the model rewrites &lt;code&gt;10&lt;/code&gt; as &lt;code&gt;10.00&lt;/code&gt;. I fixed a small problem they found and invited them to look for more. They did.&lt;/p&gt;

&lt;p&gt;Their next message said that in a race, 10 was approved but 20 reached the function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing it
&lt;/h2&gt;

&lt;p&gt;I expected a rare thread race. I reproduced it, and it was worse: no threads were needed.&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;with&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;approve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;pay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# process dies here: the "yes" is spent, nothing else is recorded
&lt;/span&gt;
&lt;span class="c1"&gt;# new process
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;pay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# runs, and 20 is paid
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The cause
&lt;/h2&gt;

&lt;p&gt;Approval had a shortcut for retries. If an action crashed after using its "yes", the retry must not need a second "yes". So the code asked: has this key already spent an approval?&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="n"&gt;spent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;effect_approved&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&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;spent&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;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# this action already used its yes, carry on
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a normal call, the key is a hash of the arguments, so this is safe. With an explicit key, the key is the same for any arguments. The check answered "has this key used a yes?" when the question that matters is "was that yes for these arguments?"&lt;/p&gt;

&lt;p&gt;The fix is one more condition: the spent approval must match the arguments about to run. Otherwise the call is refused.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why my tests missed it
&lt;/h2&gt;

&lt;p&gt;I had tests for approvals. I had tests for crashes. I had tests for explicit keys. I even re-inserted old bugs on purpose to check that the tests catch them. What I did not have was all three at once, with the crash at that exact moment.&lt;/p&gt;

&lt;p&gt;A crash test only finds what happens at the point where you crash. Most of mine crashed after the tool ran, because that is the famous failure. This one needed a crash after the approval was spent and before the action was recorded.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell anyone building approval flows
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check the approval at the moment of acting, against the real arguments.&lt;/strong&gt; Not at the resume, and not against a flag saying "approved".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be suspicious of every shortcut for retries.&lt;/strong&gt; Each one says "we already checked this". Ask what exactly was checked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;List the gaps between your steps, and crash in each one.&lt;/strong&gt; For an approved action there are at least three: after the "yes" is spent, after the intent is recorded, and after the action returns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat a key as naming one action.&lt;/strong&gt; If it comes back with other arguments, raise an error. Payment APIs such as Stripe do this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask strangers to break it, and make that easy.&lt;/strong&gt; My own tests had passed on every release. One careful reader found it in days.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What happened next
&lt;/h2&gt;

&lt;p&gt;The fix shipped in version 0.5.2, with a check that fails if the bug comes back, and a &lt;a href="https://github.com/salahmakboul/call4agents/security/advisories/GHSA-gxg5-95g9-xw4w" rel="noopener noreferrer"&gt;public security advisory&lt;/a&gt; that credits the reporter. The full discussion is in &lt;a href="https://github.com/salahmakboul/call4agents/issues/1" rel="noopener noreferrer"&gt;the issue&lt;/a&gt;. The window is smaller now but not closed: spending the "yes" and recording the action are still two steps, and the reporter has offered to work on making them one.&lt;/p&gt;

&lt;p&gt;If you run an approval flow, try the same test on your own agent: kill the process right after the approval is used, then resume with different arguments. I'd like to hear what you find.&lt;/p&gt;

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