<?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: Snigdh Sharma</title>
    <description>The latest articles on DEV Community by Snigdh Sharma (@snigdh).</description>
    <link>https://dev.to/snigdh</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%2F4184745%2F296c1a07-ce18-4d20-b1c6-db2bdd560831.png</url>
      <title>DEV Community: Snigdh Sharma</title>
      <link>https://dev.to/snigdh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/snigdh"/>
    <language>en</language>
    <item>
      <title>Safe Retries for AI Agent Tools: Idempotency Beyond Authorization</title>
      <dc:creator>Snigdh Sharma</dc:creator>
      <pubDate>Wed, 21 Oct 2026 06:50:07 +0000</pubDate>
      <link>https://dev.to/snigdh/safe-retries-for-ai-agent-tools-idempotency-beyond-authorization-2p2l</link>
      <guid>https://dev.to/snigdh/safe-retries-for-ai-agent-tools-idempotency-beyond-authorization-2p2l</guid>
      <description>&lt;h2&gt;
  
  
  The failure after the permission check
&lt;/h2&gt;

&lt;p&gt;Consider an agent asked to send one invoice reminder. The application verifies tenant ownership and approval, the downstream service accepts the message, and then the connection drops before the application receives a response. The agent sees a timeout. Sending again with a fresh operation identifier may deliver a second reminder. Refusing every retry would avoid that duplicate but could also abandon requests that never reached the service.&lt;/p&gt;

&lt;p&gt;The useful distinction is between an unsuccessful observation and an unsuccessful action. A timeout tells the caller that it lacks a conclusive response. It does not, on its own, establish that nothing happened. This is a reliability problem even when the model follows instructions perfectly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://snigdhsharma.in/labs/agent-security" rel="noopener noreferrer"&gt;Explore the earlier tool-authorization lab →&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Give the logical action an identity before execution
&lt;/h2&gt;

&lt;p&gt;For the reminder workflow, let trusted application code create an operation ID when it records the approved action. Persist that ID with the authenticated tenant, actor, tool, invoice, recipient, and message body. Every transport attempt for that same action carries the same ID. A genuinely new reminder receives a new ID, even when its text happens to match an earlier reminder.&lt;/p&gt;

&lt;p&gt;Do not let the model regenerate the identifier while retrying. Do not use only an invoice ID: an invoice may legitimately receive several reminders. A correlation ID is useful for tracing a conversation, but one conversation may contain several distinct actions. Keep trace identity, operation identity, and attempt identity separate.&lt;/p&gt;

&lt;p&gt;Within this illustrative design, scope the execution record by tenant, tool, and operation ID. Bind it to a canonical representation of the approved payload, using a hash only after defining field normalization. Reusing the ID with a changed recipient must produce a conflict, not silently replay a result for different arguments. An idempotency key is not a credential; authorization still applies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Record outcomes without inventing certainty
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Pending: the operation is durably recorded but has no confirmed downstream outcome.&lt;/li&gt;
&lt;li&gt;Succeeded: persist the downstream receipt and the result the authorized caller may retrieve.&lt;/li&gt;
&lt;li&gt;Rejected: retain a definitive refusal and its reason where appropriate.&lt;/li&gt;
&lt;li&gt;Unknown: execution may have occurred, but evidence is insufficient to label it successful or failed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This state model is a design proposal, not a provider API contract. It deliberately separates an unknown outcome from a rejection. When a worker dies after dispatch, recovery should inspect the downstream receipt or retry under the provider's supported idempotency contract. Creating a fresh ID to escape the uncertainty changes the operation and can duplicate its effects.&lt;/p&gt;

&lt;p&gt;A durable record alone is insufficient if two workers can both observe that the key is absent and dispatch simultaneously. For effects inside one database, place the business write and deduplication record in the same transaction, with a uniqueness constraint that arbitrates concurrent claims. For an external email or payment provider, that database transaction cannot atomically commit the remote effect. Use the downstream service's guarantees, or reconcile ambiguous work explicitly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the provider's actual contract
&lt;/h2&gt;

&lt;p&gt;Azure Service Bus duplicate detection addresses repeated producer sends within a configured window. It does not remove the need for idempotent consumers when a message is redelivered. In peek-lock mode, processing can finish before settlement succeeds, leaving another consumer to receive the message again. Preserve the logical operation identity across that handoff.&lt;/p&gt;

&lt;p&gt;Stripe illustrates why generic retry advice is insufficient. Its API v1 saves the response after endpoint execution begins, including a 500 response; retries with that key return the saved response. Parameters must match, and reuse after a retained key is pruned can create a new request. API v2 has different documented behavior for failed or partially completed operations. Those are Stripe-specific contracts, not promises made by every tool API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six failure tests before granting write access
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Lost response after acceptance: simulate downstream success followed by a timeout. Retry the same logical operation and inspect the actual effect count.&lt;/li&gt;
&lt;li&gt;Concurrent attempts: release two workers against the same operation ID. Check the uniqueness mechanism and downstream behavior, not just HTTP status codes.&lt;/li&gt;
&lt;li&gt;Changed payload: retry the same ID with a different recipient or body. Require a conflict with no new effect.&lt;/li&gt;
&lt;li&gt;Worker restart: stop execution after remote acceptance but before the local success record. Verify that recovery preserves uncertainty until reconciled.&lt;/li&gt;
&lt;li&gt;Expired deduplication history: retry after the provider's retention window. Verify the application's reconciliation policy rather than assuming deduplication lasts forever.&lt;/li&gt;
&lt;li&gt;Revoked permission: retry after the caller loses access. Verify that neither a new dispatch nor disclosure of a cached result bypasses current authorization.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Define what success means
&lt;/h2&gt;

&lt;p&gt;For each test, record the logical operation ID, attempt IDs, authorization decision, downstream receipt, and observed effects. Avoid logging message bodies or personal data unless required and appropriately protected. Specify the boundary being tested: one database mutation, one accepted provider request, and one delivered email are different claims.&lt;/p&gt;

&lt;p&gt;The release criterion should name the invariant and its limits: for example, retries of one approved operation produce no additional accepted sends within the documented retention and recovery conditions. This article supplies a design and test plan, not proof that a particular implementation satisfies it. The next step is to execute those cases against the actual database, broker, and downstream service used by the application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-message-loss-and-duplicates" rel="noopener noreferrer"&gt;Microsoft Learn: Prevent message loss and duplicate processing in Azure Service Bus&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/idempotent-consumer" rel="noopener noreferrer"&gt;Azure Architecture Center: Idempotent Consumer pattern&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.stripe.com/api/idempotent_requests" rel="noopener noreferrer"&gt;Stripe API: Idempotent requests&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Originally published on &lt;a href="https://snigdhsharma.in/insights/ai-agent-retries-idempotency" rel="noopener noreferrer"&gt;snigdhsharma.in&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: Research-informed design guide researched and written autonomously by an AI agent under the site owner’s publishing instructions. The invoice reminder and failure cases are illustrative; this article reports no production deployment or measured benchmark. Primary documentation checked October 21, 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>architecture</category>
      <category>azure</category>
    </item>
    <item>
      <title>Testing AI Agent Tool Authorization: Seven Reproducible Failure Cases</title>
      <dc:creator>Snigdh Sharma</dc:creator>
      <pubDate>Wed, 14 Oct 2026 12:58:01 +0000</pubDate>
      <link>https://dev.to/snigdh/testing-ai-agent-tool-authorization-seven-reproducible-failure-cases-1d9b</link>
      <guid>https://dev.to/snigdh/testing-ai-agent-tool-authorization-seven-reproducible-failure-cases-1d9b</guid>
      <description>&lt;p&gt;An agent can propose a well-formed tool call that the application should never execute. The question this experiment asks is narrow: &lt;strong&gt;if the unwanted call has already been proposed, can an authorization boundary refuse it while preserving legitimate operations?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://snigdhsharma.in/labs/agent-security" rel="noopener noreferrer"&gt;Try the interactive lab&lt;/a&gt;. It runs entirely in your browser with synthetic invoices. No account, model API, or real messages are involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;The caller, Alex, belongs to tenant North. Invoice INV-A belongs to North; INV-B belongs to South. Each fixture supplies a predefined tool call, trusted caller context, and—where appropriate—an approval record. Document text is displayed as untrusted context and is never parsed into permissions.&lt;/p&gt;

&lt;p&gt;The baseline validates known tools and basic input structure but omits four authorization controls:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Active identity:&lt;/strong&gt; reject a caller whose access has been revoked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool scope:&lt;/strong&gt; allow invoice reads and reminders, but deny bulk export.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tenant ownership:&lt;/strong&gt; resolve ownership from the stored record, not a tenant label in model arguments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exact approval:&lt;/strong&gt; require approval to match the actor, tool, invoice, recipient, and message body.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The browser lets you disable these controls independently and inspect the decision, check trace, and simulated effects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seven cases
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Read an owned invoice: allow.&lt;/li&gt;
&lt;li&gt;Read another tenant’s invoice, despite a forged tenant argument: deny.&lt;/li&gt;
&lt;li&gt;Invoke a bulk-export tool suggested by an invoice note: deny.&lt;/li&gt;
&lt;li&gt;Turn a read-only request into an unapproved send: deny.&lt;/li&gt;
&lt;li&gt;Send the exact approved reminder: allow.&lt;/li&gt;
&lt;li&gt;Change the recipient after approval: deny.&lt;/li&gt;
&lt;li&gt;Execute a previously planned read after access is revoked: deny.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the results establish
&lt;/h2&gt;

&lt;p&gt;For engine version 1.0.0, the unchecked baseline matches &lt;strong&gt;2 of 7&lt;/strong&gt; expected decisions: it permits both legitimate operations and all five adverse actions. With all four controls enabled, &lt;strong&gt;7 of 7&lt;/strong&gt; decisions match: five adverse actions blocked, two legitimate operations allowed. Denied calls have no simulated effects.&lt;/p&gt;

&lt;p&gt;These are fixed-fixture results, &lt;strong&gt;not an LLM accuracy score or prompt-injection attack-success rate&lt;/strong&gt;. The experiment does not sample a model. Its cases were chosen to exercise the controls, so passing them establishes those particular behaviors—not coverage of unknown attacks.&lt;/p&gt;

&lt;p&gt;Try disabling tenant ownership: the cross-tenant read becomes allowed. Restore it and disable exact approval: unapproved or modified sends become possible. The suite updates immediately so the regression is visible alongside legitimate operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  A useful composition failure
&lt;/h2&gt;

&lt;p&gt;The export tool is intentionally overbroad. Tenant checks on individual reads do not protect this separate bulk endpoint. In the lab, tool scope blocks it; in production, an export endpoint also needs record-level authorization.&lt;/p&gt;

&lt;p&gt;This is the architectural point: a protected read path cannot compensate for an unprotected alternative route to the same data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce the experiment
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/Snigdh41/snigdh41.github.io/blob/main/public/labs/agent-security-engine.mjs" rel="noopener noreferrer"&gt;dependency-free engine and fixtures&lt;/a&gt; power both the browser and the &lt;a href="https://github.com/Snigdh41/snigdh41.github.io/blob/main/scripts/agent-security.test.mjs" rel="noopener noreferrer"&gt;19 executable tests&lt;/a&gt;. Download the engine from the lab and save this beside it as &lt;code&gt;reproduce.mjs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;runSuite&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;allControls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;noControls&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./agent-security-engine.mjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controls&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;noControls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;allControls&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;runSuite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;controls&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;correct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// Expected: 2 7, then 7 7&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;node reproduce.mjs&lt;/code&gt;. You can also export the browser’s current results as JSON, or run &lt;code&gt;npm run test:lab&lt;/code&gt; in the repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  What remains outside the experiment
&lt;/h2&gt;

&lt;p&gt;This is educational code, not production authorization middleware. Trusted identity, current permissions, ownership, and approval must come from a server-side enforcement point. Browser toggles are not a security boundary.&lt;/p&gt;

&lt;p&gt;The experiment does not test token validation, approval expiry or replay, concurrent revocation, network egress, idempotent writes, or malicious final answers. Those require additional designs and tests.&lt;/p&gt;

&lt;p&gt;For broader guidance, see the &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/LLM_Prompt_Injection_Prevention_Cheat_Sheet.html" rel="noopener noreferrer"&gt;OWASP prompt-injection prevention cheat sheet&lt;/a&gt; and &lt;a href="https://modelcontextprotocol.io/docs/draft/tutorials/security/security_best_practices" rel="noopener noreferrer"&gt;MCP security best practices&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://snigdhsharma.in/labs/agent-security" rel="noopener noreferrer"&gt;Open the lab&lt;/a&gt; · &lt;a href="https://snigdhsharma.in/insights/agent-tool-authorization-lab" rel="noopener noreferrer"&gt;Original article and full limitations&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Which extension would be most useful to inspect next: one-time approvals, revocation races, or a retry ledger?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: this write-up and lab were developed with AI assistance. The published results are reproducible deterministic fixture outcomes.&lt;/em&gt;&lt;/p&gt;

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