<?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: 10xGraph</title>
    <description>The latest articles on DEV Community by 10xGraph (@10xgraph).</description>
    <link>https://dev.to/10xgraph</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%2F4199781%2F17b1cdea-1bec-4364-aa5f-e51c2353375e.jpeg</url>
      <title>DEV Community: 10xGraph</title>
      <link>https://dev.to/10xgraph</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/10xgraph"/>
    <language>en</language>
    <item>
      <title>[Boost]</title>
      <dc:creator>10xGraph</dc:creator>
      <pubDate>Sun, 15 Nov 2026 07:09:31 +0000</pubDate>
      <link>https://dev.to/10xgraph/-1o23</link>
      <guid>https://dev.to/10xgraph/-1o23</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/10xgraph/a-prompt-injected-model-cannot-skip-your-approval-step-48lc" class="crayons-story__hidden-navigation-link"&gt;A prompt-injected model cannot skip your approval step&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/10xgraph" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F4199781%2F17b1cdea-1bec-4364-aa5f-e51c2353375e.jpeg" alt="10xgraph profile" class="crayons-avatar__image" width="800" height="420"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/10xgraph" class="crayons-story__secondary fw-medium m:hidden"&gt;
              10xGraph
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                10xGraph
                
                
              
              &lt;div id="story-author-preview-content-5039944" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/10xgraph" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F4199781%2F17b1cdea-1bec-4364-aa5f-e51c2353375e.jpeg" class="crayons-avatar__image" alt="" width="800" height="420"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;10xGraph&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/10xgraph/a-prompt-injected-model-cannot-skip-your-approval-step-48lc" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Nov 15&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/10xgraph/a-prompt-injected-model-cannot-skip-your-approval-step-48lc" id="article-link-5039944"&gt;
          A prompt-injected model cannot skip your approval step
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/security"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;security&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/llm"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;llm&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/python"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;python&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/10xgraph/a-prompt-injected-model-cannot-skip-your-approval-step-48lc" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;2&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/10xgraph/a-prompt-injected-model-cannot-skip-your-approval-step-48lc#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              2&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            5 min read
          &lt;/small&gt;
          
          &lt;div class="only-sidebar-menu-item trusted-visible-block print-hidden align-center"&gt;
                
                    
                
          &lt;/div&gt;

        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;/div&gt;


</description>
    </item>
    <item>
      <title>A prompt-injected model cannot skip your approval step</title>
      <dc:creator>10xGraph</dc:creator>
      <pubDate>Sun, 15 Nov 2026 07:06:09 +0000</pubDate>
      <link>https://dev.to/10xgraph/a-prompt-injected-model-cannot-skip-your-approval-step-48lc</link>
      <guid>https://dev.to/10xgraph/a-prompt-injected-model-cannot-skip-your-approval-step-48lc</guid>
      <description>&lt;p&gt;A human-in-the-loop design usually has the same shape: the model proposes an action, the graph stops at an approval step, a person says yes or no, and only then does the tool run. The guarantee you want is that the model has no way to reach the tool without passing the stop.&lt;/p&gt;

&lt;p&gt;That guarantee holds only if every path around the stop is closed, including the paths a model can influence by text alone. A model that has read a hostile web page or a poisoned document can be steered into producing unusual output: a tool name you did not expect, a routing hint, a message that looks like a human answer. This post lists the places where 10xGraph decides on the graph's side instead of trusting that output, and what you still have to do yourself. It describes behaviour and design, not ways to attack a particular app.&lt;/p&gt;

&lt;p&gt;10xGraph is open-source, self-hosted infrastructure for AI applications. You write the agents, tools and tasks; 10xGraph runs them behind an API server with authentication, role-based access, tenant isolation, durable state in Postgres and Redis, background jobs, metrics and tracing, and generates Docker and Kubernetes deploy files. MIT licensed, by 10xScale. Formerly Agentflow.&lt;/p&gt;

&lt;p&gt;The reference pages are &lt;a href="https://10xgraph.com/docs/concepts/interrupts" rel="noopener noreferrer"&gt;Interrupts&lt;/a&gt; and the &lt;a href="https://10xgraph.com/docs/production/security-model" rel="noopener noreferrer"&gt;security model&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handoffs only go where a handoff tool says
&lt;/h2&gt;

&lt;p&gt;Handoff is how one agent passes the conversation to another, through a tool made with &lt;code&gt;create_handoff_tool&lt;/code&gt;. If any tool call named &lt;code&gt;transfer_to_&amp;lt;X&amp;gt;&lt;/code&gt; moved the run to node &lt;code&gt;&amp;lt;X&amp;gt;&lt;/code&gt;, a steered model could name a node that your edges never lead to, and that includes a node behind an approval step.&lt;/p&gt;

&lt;p&gt;So a handoff is honoured only when the ToolNode holds a tool of that name created by &lt;code&gt;create_handoff_tool&lt;/code&gt;. Any other &lt;code&gt;transfer_to_*&lt;/code&gt; call is an ordinary tool call, and if no such tool exists it gets the usual "Tool not found" result.&lt;/p&gt;

&lt;p&gt;The same rule covers &lt;code&gt;Command(goto=...)&lt;/code&gt; returned from your own code. A target that is neither a node in the graph nor &lt;code&gt;END&lt;/code&gt; raises &lt;code&gt;GraphError&lt;/code&gt; with code &lt;code&gt;GRAPH_007&lt;/code&gt;, naming the node it came from and the unknown target.&lt;/p&gt;

&lt;p&gt;Starting a run at a chosen node with &lt;code&gt;current_node&lt;/code&gt; is a separate, intended feature (a frontend can send the agent the user picked). If a node needs an entry guard, enforce it inside the graph. The change above is about what the model can do, not what your caller can do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tools hidden by &lt;code&gt;tools_tags&lt;/code&gt; cannot be called
&lt;/h2&gt;

&lt;p&gt;An Agent can advertise only the tools that match &lt;code&gt;tools_tags&lt;/code&gt;. Showing the model only the tagged tools is not enough on its own, because the ToolNode could still run any registered name the model happened to guess.&lt;/p&gt;

&lt;p&gt;So the Agent records the names it advertised on the assistant message (&lt;code&gt;metadata["allowed_tools"]&lt;/code&gt;), and the dispatch step refuses a call to anything outside that list with the same "Tool not found" error. If you use tags to keep a destructive tool away from a low-trust agent, the tag holds at run time and not just in the prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  A tool name cannot be claimed twice
&lt;/h2&gt;

&lt;p&gt;When a ToolNode also connects to an MCP server, a remote tool could otherwise take over the name of a local tool, so calls meant for your function would go to the server. A collision between an MCP tool and a local or remote tool on the same ToolNode raises &lt;code&gt;ValueError&lt;/code&gt; ("Rename one of them"), and the check runs outside the code that swallows connection errors, so it cannot be hidden by a failed server ping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Forged tool results are refused
&lt;/h2&gt;

&lt;p&gt;Remote tools run on the client, and the client sends the result back to resume the run. That is the one case where the API accepts a message with the &lt;code&gt;tool&lt;/code&gt; role, and it is narrow.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Client messages may only have the &lt;code&gt;user&lt;/code&gt; role. The &lt;code&gt;assistant&lt;/code&gt; and &lt;code&gt;system&lt;/code&gt; roles are refused, and so is a &lt;code&gt;tool&lt;/code&gt; message that carries anything other than tool results.&lt;/li&gt;
&lt;li&gt;A tool result is accepted only for a remote tool call that is waiting for one on that thread. A result for a call the model never made, a second result for the same call, or a result for a server tool is refused.&lt;/li&gt;
&lt;li&gt;A client tool can never carry the name of a server tool. Otherwise a client could declare a tool with the name of the one paused for approval and post a "result" for it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The effect is that the approval path cannot be satisfied by pretending to be the model or the tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  A resume answers one specific question
&lt;/h2&gt;

&lt;p&gt;A resume carries a value, and the value can be bound to the prompt it answers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The client can send &lt;code&gt;interrupt_id&lt;/code&gt; with the resume on invoke, stream and WebSocket. If the thread is not paused, or is paused at another interrupt (a stale tab, a retried request after the first answer was consumed), the API answers 409 and records nothing. This is how a late "yes" fails to approve a different request.&lt;/li&gt;
&lt;li&gt;The core checks the id again and, when the interrupt declared a &lt;code&gt;response_schema&lt;/code&gt; (a JSON Schema), validates the value against it. A mismatch raises &lt;code&gt;InterruptMismatchError&lt;/code&gt; (&lt;code&gt;INTERRUPT_001&lt;/code&gt;), and a value that does not satisfy the schema raises &lt;code&gt;ResumeValidationError&lt;/code&gt; (&lt;code&gt;INTERRUPT_002&lt;/code&gt;). Nothing is recorded in either case.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The API checks the id first and answers 409 before a stream starts. It also passes the id to the core, which enforces it again.&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;from&lt;/span&gt; &lt;span class="n"&gt;tenxgraph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utils&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;interrupt&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;str&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="nf"&gt;interrupt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Refund ${amount}?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"tool_approval"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;response_schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"approved"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"boolean"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"approved"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&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="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"approved"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"Refund declined"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;issue_refund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;interrupt_id&lt;/code&gt;, a resume answers whatever the thread is paused at, as before. Send the id from any client where a person can have two tabs open.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you still own
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;None of this makes a model trustworthy. It removes shortcuts around your graph.&lt;/li&gt;
&lt;li&gt;Authentication, role-based access and ownership are separate layers, covered in the &lt;a href="https://10xgraph.com/docs/production/security-model" rel="noopener noreferrer"&gt;security model&lt;/a&gt;. Another user's thread answers 404. Permissions apply per route; there are no per-tool permissions, so decide inside the tool who may do what.&lt;/li&gt;
&lt;li&gt;A tool that must be safe against a steered model should still validate its own arguments and use idempotency keys. See &lt;a href="https://10xgraph.com/docs/concepts/interrupts" rel="noopener noreferrer"&gt;Interrupts&lt;/a&gt; for how to place the stop before the tool, not after it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does this stop prompt injection?
&lt;/h3&gt;

&lt;p&gt;No. Nothing stops a model from being steered by text it reads. These controls limit what a steered model can do to the parts of the graph you did not give it: the route it takes, the tools it can call and the approval a human gives.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can a client approve an interrupt by sending a tool result?
&lt;/h3&gt;

&lt;p&gt;No. The API accepts a tool result only as the answer to a remote tool call that is waiting for one. A result for a server tool, for a call the model never made, or a second result for the same call is refused.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does interrupt_id add to a resume?
&lt;/h3&gt;

&lt;p&gt;It ties the answer to the exact prompt the human saw. If the thread is paused at a different interrupt, or not paused at all, the API answers 409 and nothing is recorded.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://10xgraph.com/blog/approval-step-prompt-injection" rel="noopener noreferrer"&gt;10xGraph blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

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