<?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: shamik123</title>
    <description>The latest articles on DEV Community by shamik123 (@shamik123).</description>
    <link>https://dev.to/shamik123</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%2F1480273%2F12892147-6f2f-46f2-8155-81954e3e4937.png</url>
      <title>DEV Community: shamik123</title>
      <link>https://dev.to/shamik123</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shamik123"/>
    <language>en</language>
    <item>
      <title>Your RAG answer changed. Which step changed it?</title>
      <dc:creator>shamik123</dc:creator>
      <pubDate>Tue, 13 Oct 2026 13:00:25 +0000</pubDate>
      <link>https://dev.to/shamik123/your-rag-answer-changed-which-step-changed-it-1dka</link>
      <guid>https://dev.to/shamik123/your-rag-answer-changed-which-step-changed-it-1dka</guid>
      <description>&lt;p&gt;A question that used to be answered correctly stopped being answered.&lt;/p&gt;

&lt;p&gt;The question was about a returns policy: &lt;em&gt;how long do customers have to return an item?&lt;/em&gt; Last week the pipeline said &lt;strong&gt;ninety days&lt;/strong&gt;. This week it said &lt;strong&gt;"not stated in the retrieved evidence."&lt;/strong&gt; Nobody touched the prompt. Nobody changed the model. The documents were the same.&lt;/p&gt;

&lt;p&gt;What did change was a library upgrade in the chunker, from version 3.4 to 3.5, with three default settings different. That is the kind of change that ships every week without anyone thinking about it.&lt;/p&gt;

&lt;p&gt;The part that matters is this. &lt;strong&gt;The passage with the answer was still in the index, with the same digest. It had moved to position nine out of ten, and the search never reached it.&lt;/strong&gt; Nothing failed and nothing logged an error. The answer simply changed.&lt;/p&gt;

&lt;p&gt;If you build RAG or agent pipelines, you've had this. The hard part isn't noticing. It's finding where.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why logs and traces don't answer it
&lt;/h2&gt;

&lt;p&gt;Logs are what your system says about itself. Traces add timing and structure. Both are good at "is it slow?" and "did it throw?". Neither is good at "what is different between yesterday's run and today's?", because nobody kept the exact bytes each step read and wrote.&lt;/p&gt;

&lt;p&gt;So the investigation becomes archaeology. You diff config files, check package versions and re-run things by hand, then try to remember what the index looked like last Tuesday. With five or six steps between the question and the answer (load, split, embed, index, retrieve, answer), any of them can be the cause, and each one can be cheap or expensive to check.&lt;/p&gt;

&lt;h2&gt;
  
  
  A deterministic record around a non-deterministic model
&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%2F8tas5zq2e7dwj1bi4cd3.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%2F8tas5zq2e7dwj1bi4cd3.png" alt="How onetrace works: a deterministic record around a non-deterministic model" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The model can stay non-deterministic. The record around it is deterministic.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The idea behind onetrace is small. Every step of the pipeline leaves a &lt;strong&gt;receipt&lt;/strong&gt; as it runs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;digests of what went in and what came out;&lt;/li&gt;
&lt;li&gt;the tool and version that did the work;&lt;/li&gt;
&lt;li&gt;the settings it ran with.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The receipts are hash-chained into a run folder on your own disk, so a record that was edited afterwards no longer checks.&lt;/p&gt;

&lt;p&gt;Recording a step is one decorator. Recording a run is one more:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;onetrace&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ot&lt;/span&gt;

&lt;span class="nd"&gt;@ot.stage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retrieve&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;instrument&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;word-overlap&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;onetrace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                         &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retriever&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;top_k&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="nd"&gt;@ot.stage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;instrument&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;first-passage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;onetrace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model-call&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;passages&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="nd"&gt;@ot.run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retrieve&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your code runs as before. &lt;code&gt;ONETRACE_DISABLE=1&lt;/code&gt; turns it off without touching the code.&lt;/p&gt;

&lt;p&gt;The model doesn't have to become deterministic. That's the point: &lt;strong&gt;variation is allowed, every change is caught as a different digest, and the change is located.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparing two runs: same, same, same… first difference
&lt;/h2&gt;

&lt;p&gt;With two recorded runs, the comparison is mechanical. Walk the steps in order and compare each step's recorded output:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Same:&lt;/strong&gt; the output digest matches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First difference:&lt;/strong&gt; the first step, in order, whose output differs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Changed after it:&lt;/strong&gt; still different further down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconverged:&lt;/strong&gt; different work upstream, the same output here. This is the case evals never see, because evals only look at the final answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the returns-policy incident, the comparison named &lt;code&gt;split&lt;/code&gt; as the first difference, with the three chunker settings that changed. Everything after it was marked. Then the text diff showed exactly how the returns passage had been cut into two chunks.&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%2Faf9hbw4c269xaubftd8q.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%2Faf9hbw4c269xaubftd8q.png" alt="Text diff of the changed chunk" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Not just which step. The exact text that changed, with each side checked against its receipt before it's shown.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Does it scale? The cost is in steps, not documents
&lt;/h2&gt;

&lt;p&gt;The obvious worry is that this works on a toy and falls over on real data. So we ran the same chunker change on a much bigger corpus: &lt;strong&gt;47,430 public SEC filings&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The comparison doesn't read the data again. Each step's outputs were hashed while the runs were recorded, so comparing two runs means comparing one digest per step. Our pipeline had nine steps. &lt;strong&gt;The first difference was found in the same nine comparisons at 13 documents, at 20,000 and at 47,430.&lt;/strong&gt; A five-step pipeline would take five. The cost grows with the number of steps, not the size of the data.&lt;/p&gt;

&lt;p&gt;It also gave the &lt;strong&gt;blast radius&lt;/strong&gt; from index digests alone, without opening a single document:&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%2Fasibgathz0383x92y3zm.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%2Fasibgathz0383x92y3zm.png" alt="Scale table: two chunker changes on 47,430 filings" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Two chunker changes on 47,430 filings, both found in the same nine comparisons, with documents changed read from index digests alone.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Two honest notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The final answer was identical in both large runs, but that isn't evidence of robustness.&lt;/strong&gt; It was a refusal both times ("not stated in the retrieved evidence"), because the retrieved evidence was useless in both. A stable refusal tells you nothing about how robust a system is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;These large runs used an earlier research build of the same method,&lt;/strong&gt; and the figures are read from its own output. The comparison that ships in onetrace today walks steps the same way.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  A surprise: batch size alone changed what was retrieved
&lt;/h2&gt;

&lt;p&gt;While testing which steps can be re-run to the same bytes, we found something we hadn't expected. With the same code, data and model, &lt;strong&gt;changing only the embedding batch size changed which documents were retrieved&lt;/strong&gt;. Floating-point differences of around 10⁻⁷ were enough to reorder near-ties. Every log was green.&lt;/p&gt;

&lt;p&gt;That's why onetrace doesn't take your word for which steps are reproducible. A step that calls a hosted model is marked as not reproducible, and the comparison treats it that way. When such a step's output differs and nothing recorded about it changed, the console looks past it and names the first later step that does have a recorded change.&lt;/p&gt;
&lt;h2&gt;
  
  
  The console: the same comparison, in your browser
&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%2F26h1h8fb2h18naaipmwt.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%2F26h1h8fb2h18naaipmwt.png" alt="The console showing your own runs" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;After setup, one command opens your own runs in the browser. New runs appear on their own.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;onetrace-console 0.1.1 is out. It reads your run folders and shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;an overview&lt;/strong&gt; of every run and what the evidence for each one establishes;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;one page per run,&lt;/strong&gt; with each check spelled out;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;comparisons,&lt;/strong&gt; with the first difference marked, the settings that changed there, and the exact text that changed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It runs on your machine, listens on 127.0.0.1 only, and makes no network calls. Every page gives the command that repeats its check, so the same comparison can gate a CI job.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;onetrace onetrace-console
onetrace-console demo &lt;span class="nt"&gt;--open&lt;/span&gt;        &lt;span class="c"&gt;# the demo's 30 runs, locally&lt;/span&gt;
onetrace-console runs &lt;span class="nt"&gt;--open&lt;/span&gt;        &lt;span class="c"&gt;# your own runs folder&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or try it without installing: &lt;strong&gt;&lt;a href="https://demo.oneproof.dev" rel="noopener noreferrer"&gt;demo.oneproof.dev&lt;/a&gt;&lt;/strong&gt; is a read-only copy with 30 real runs from four small example pipelines. Start with Comparisons.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't judge truth.&lt;/strong&gt; A faithfully recorded wrong answer still verifies. It shows what ran, not whether the answer was right.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It never says why.&lt;/strong&gt; It shows where two runs differ and what was recorded there. The why is still your job, but a much smaller one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It only sees the steps you decorate,&lt;/strong&gt; and it tells you which those are.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Not checked" is never "passed".&lt;/strong&gt; Every gap is named, in the product and in the docs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next: who allowed it
&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%2Fbuedtsdl5fl5cqi4dklv.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%2Fbuedtsdl5fl5cqi4dklv.png" alt="Target design: policy decision and refused bound permit" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Coming next, target design: a policy decision, and a bound permit refused because the request changed on the way.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For agents that act, not just answer, the next question isn't only "what changed?" but "who allowed it?". The target design adds a decision point, onedoor. It checks each action an agent proposes against policy, condition by condition. A &lt;strong&gt;bound permit&lt;/strong&gt; then carries that decision to the system that acts, tied to one exact request. If the request changes on the way, the recipient refuses it and nothing happens, and the refusal is recorded with the run. This isn't in the current release, and the image above is labelled as such.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Live demo: &lt;a href="https://demo.oneproof.dev" rel="noopener noreferrer"&gt;demo.oneproof.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs and a 15-minute get started: &lt;a href="https://oneproof.dev" rel="noopener noreferrer"&gt;oneproof.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source (Apache-2.0): &lt;a href="https://github.com/shamiksaharcciit-oss/onetrace-console" rel="noopener noreferrer"&gt;github.com/shamiksaharcciit-oss/onetrace-console&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are guides for plain Python, LangChain, LlamaIndex and Langflow, and a recipe your coding agent can follow to add onetrace for you.&lt;/p&gt;

&lt;p&gt;If you debug RAG or agent pipelines, I'd like to hear what you'd need it to record that it doesn't, and where the decorators get in the way.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://medium.com/@shamik.saha.rcciit/your-rag-answer-changed-which-step-changed-it-940883eca8fd" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>rag</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Gave an LLM the Keys to My House. Here's the Guardrail Engine That Keeps It Honest</title>
      <dc:creator>shamik123</dc:creator>
      <pubDate>Tue, 11 Aug 2026 15:18:31 +0000</pubDate>
      <link>https://dev.to/shamik123/i-gave-an-llm-the-keys-to-my-house-heres-the-guardrail-engine-that-keeps-it-honest-1938</link>
      <guid>https://dev.to/shamik123/i-gave-an-llm-the-keys-to-my-house-heres-the-guardrail-engine-that-keeps-it-honest-1938</guid>
      <description>&lt;p&gt;&lt;em&gt;What I learned building a guardrail engine for an agent that runs my house — and how the same engine bolts onto an AI gateway.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This year I gave an LLM agent the keys to my house. Battery schedules, climate...&lt;/p&gt;

&lt;p&gt;This year I gave an LLM agent the keys to my house. Battery schedules, climate setpoints, calendar, a view of my bank transactions. Not as a demo — as the system that actually runs things, every day, while I'm at work.&lt;/p&gt;

&lt;p&gt;Which meant I had to answer a question most agent frameworks politely defer:&lt;/p&gt;

&lt;p&gt;what happens when the model is wrong?&lt;/p&gt;

&lt;p&gt;Not wrong in the benchmark sense. Wrong at 3 a.m., wrong about my heating in February, wrong with my money. The moment an agent can &lt;em&gt;act&lt;/em&gt;, hallucination stops being a quality metric and becomes a physical event. And the standard answer, "we prompt it to be careful," is not an answer at all, for a reason worth stating plainly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An instruction is not a constraint.&lt;/strong&gt; To a language model, your rule is just more text that makes certain continuations more likely. It tilts a probability distribution; it cannot clamp one. A contract with a compiler is enforced. A contract with a model is strongly suggested. If your safety story lives in the&lt;br&gt;
prompt, you don't have a safety story. You have a safety mood.&lt;/p&gt;

&lt;p&gt;So I built the enforcement outside the model and ran it for months against real consequences. This post is the design that survived, the code is open source, and the last section shows the same engine governing calls inside LiteLLM, because the pattern turns out not to care whose door it guards.&lt;/p&gt;
&lt;h2&gt;
  
  
  One door
&lt;/h2&gt;

&lt;p&gt;Nothing in my system acts directly. The scheduler, the YAML rules, the LLM agent, even my own thumb on a UI button — all of them can only construct an &lt;code&gt;ActionRequest&lt;/code&gt; and submit it to one executor, which is the only code in the system permitted to call a connector. There is one door, structurally. An agent cannot bypass a checkpoint that owns the only road.&lt;/p&gt;

&lt;p&gt;This sounds obvious. Almost no framework does it. In most agent stacks, authorization lives in the prompt ("only use this tool when...") or in scattered per-tool checks, which means it lives nowhere the model can't route around.&lt;/p&gt;
&lt;h2&gt;
  
  
  Autonomy is tiered, per action — not per agent
&lt;/h2&gt;

&lt;p&gt;Every action type carries a tier in a policy table (data, not code):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tier 0 — observe.&lt;/strong&gt; Reads. Always allowed, even during an incident — &lt;em&gt;especially&lt;/em&gt; during an incident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 1 — auto-execute.&lt;/strong&gt; Reversible, low-cost, in-bounds. Lights, climate within limits, battery schedules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 2 — auto-execute under caps.&lt;/strong&gt; Rate limits and cumulative euro budgets, enforced by the engine, not trusted to the caller.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 3 — propose-and-confirm.&lt;/strong&gt; Everything irreversible, everything over cap, everything involving money — and everything &lt;em&gt;unknown&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last clause is the first hard rule: &lt;strong&gt;default-deny&lt;/strong&gt;. An action type not in the policy table isn't an error and isn't a pass. It resolves to Tier 3 and a human gets asked. Novelty is never trusted; it is escalated. When my agent invents a new way to be helpful, the system's response is "interesting —&lt;br&gt;
ask the human."&lt;/p&gt;
&lt;h2&gt;
  
  
  The rule I'd defend in front of any safety review
&lt;/h2&gt;

&lt;p&gt;Here is the design decision I'd keep if I had to throw away all the others:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An action with no registered undo cannot be auto-executed. Reversibility is not a feature. It is the precondition for autonomy.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every Tier-1 policy must declare a compensating command (restore the previous setpoint, cancel the schedule) at registration time. The policy loader refuses to boot if one is missing. And the executor checks again at runtime: an auto-tier action without a reversal is silently demoted to propose-and-confirm, with the reason logged. If it can't be taken back, a human signs off. By definition, not by vibes.&lt;/p&gt;

&lt;p&gt;Executed actions then surface a one-tap undo for fifteen minutes, and the undo itself is submitted &lt;em&gt;through the same pipeline&lt;/em&gt;, as a new request marked as a reversal of its parent. Even regret goes through the front door.&lt;/p&gt;
&lt;h2&gt;
  
  
  Order is the design
&lt;/h2&gt;

&lt;p&gt;The executor is a small deterministic state machine, and the sequence of its checks encodes every priority the system has. Out of order, the same checks would produce a subtly broken machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The kill switch is checked first — before policy lookup.&lt;/strong&gt; If it's engaged, every actionable tier clamps to propose-and-confirm. Reads stay exempt (you want eyes during an incident), and one edge case took me an evening of thinking: an &lt;em&gt;already-approved&lt;/em&gt; action arriving while the switch is engaged is&lt;br&gt;
blocked without creating a new approval — otherwise the block would spawn an approval, whose approval would be blocked, which would spawn an approval. The panic button outranks everything, including prior human consent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bounds are validated before a human ever sees a proposal.&lt;/strong&gt; Climate 17–23°C, battery power within hardware limits, enums closed, unknown parameters rejected. The point isn't just stopping the model. It's that the approval screen becomes trustworthy: a human deciding "should this happen?" should&lt;br&gt;
never also have to catch "wait, is 45 degrees insane?" The human decides &lt;em&gt;whether&lt;/em&gt;. The machine has already decided &lt;em&gt;whether it's sane&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dry-run is resolved before cap accounting&lt;/strong&gt;, for a one-line reason that took me embarrassingly long to see: a rehearsal must not spend a real budget. Every new action type starts in dry-run — two weeks of "would have executed" in the log before it's allowed to touch anything. You get to watch what your agent&lt;br&gt;
&lt;em&gt;would have done&lt;/em&gt; before it does it. I recommend this more than any other single feature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caps are reserved inside the deciding transaction.&lt;/strong&gt; SQLite, &lt;code&gt;BEGIN IMMEDIATE&lt;/code&gt;, check-and-reserve under the write lock, so two concurrent requests can never both fit under the last slot of a daily cap. Boring database discipline, absent from every agent framework I've read.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Execution is two-phase.&lt;/strong&gt; Transaction A decides, reserves the cap, and records &lt;em&gt;intent&lt;/em&gt; in the audit log. Then the connector call — the network I/O that actually flips the switch — runs outside any lock, under a hard timeout.&lt;br&gt;
Then transaction B appends the result. A hung smart-plug API can't hold the engine's database hostage, and a crash between phases leaves an honest "intended, unconfirmed" row instead of a lie.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And the audit log is append-only.&lt;/strong&gt; Decisions, results, denials, dry-runs, kill-switch blocks — typed reason codes, never updated in place. When I want to know why the heating did something weird on Tuesday, the answer is a query, not an archaeology project.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the field has, and what it calls unsolved
&lt;/h2&gt;

&lt;p&gt;I built this for one house and one user, so I read the current agent-security literature with some amusement. The gap analyses say agent authorization still lacks: enforcement in infrastructure the agent cannot bypass (rather than in&lt;br&gt;
prompts); decision types beyond allow/deny — defer, step-up; runtime controls rather than design-time configuration.&lt;/p&gt;

&lt;p&gt;The state of the art is worth naming precisely, because it's better than the think-pieces suggest and still structurally short. LiteLLM — the most widely deployed open-source AI gateway — now ships real MCP permission management:&lt;br&gt;
server-level access per key, team and org, tool allow/block lists, even allowed parameter &lt;em&gt;names&lt;/em&gt;, with a permission hierarchy where the most restrictive level wins. That's a genuine access-control system. It is also, categorically, static allow/deny. There is no defer (a call that waits for a&lt;br&gt;
human), no dry-run, no undo, no kill-switch semantics, and the parameter control stops at names: the gateway can say "this tool takes &lt;code&gt;amount_eur&lt;/code&gt;," but not "&lt;code&gt;amount_eur&lt;/code&gt; must be at most 500."&lt;/p&gt;

&lt;p&gt;That missing list is this engine's feature set. One door is the bypass answer. Tier 3 is defer as a first-class outcome, and step-up authentication is a field in the money policy (which is Tier 3 &lt;em&gt;permanently&lt;/em&gt; — some tiers should never be earned back by good behavior). The kill switch, dry-run windows and cap exhaustion are all runtime state. I don't think I'm cleverer&lt;br&gt;
than the people building agent platforms. I think I had the advantage of consequences. It's remarkable how quickly "the model is usually right" stops being an architecture when the model can open your curtains at 3 a.m.&lt;/p&gt;

&lt;p&gt;What I &lt;em&gt;don't&lt;/em&gt; have, and haven't seen anyone ship: session-aware trust — an authorizer that remembers what the agent has done this session and degrades its autonomy accordingly. That's the next idea on my list.&lt;/p&gt;
&lt;h2&gt;
  
  
  The door, installed on other people's doorways
&lt;/h2&gt;

&lt;p&gt;After extracting the engine from my house I did the refactor the&lt;br&gt;
authorization world would call overdue: I split the judge from the bailiff.&lt;/p&gt;

&lt;p&gt;The decision half — the ordered checks, the cap reservation, the intent row in the audit log — is one public function (&lt;code&gt;decide_and_reserve&lt;/code&gt;); the receipt is a second one (&lt;code&gt;report_result&lt;/code&gt;). The in-process executor is just the two composed around a connector call. Which means the enforcement can now live anywhere, because the judgment doesn't move.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First installation: a Model Context Protocol proxy.&lt;/strong&gt; MCP is where agent tool-calls are converging, and the proxy sits between any MCP host and any stdio tool server, invisible to both. Every &lt;code&gt;tools/call&lt;/code&gt; becomes an &lt;code&gt;ActionRequest&lt;/code&gt; through the full pipeline. From the demo, verbatim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3) Out of bounds: denied by the proxy, never reaches the tool:
   ERR  onedoor: 'set_thermostat' denied (reason: bounds — param
        'temperature'=30 above max 23.0). The call was not forwarded.
4) Money is Tier 3 — proposed, not forwarded:
   ERR  onedoor: 'send_payment' requires approval (tier 3;
        approval_id=1). A human can release it.
5) A human approves — only then is the call forwarded:
   ok   Sent €49.99 to webshop (simulated).
6) An unknown tool default-denies to a human:
   ERR  onedoor: 'delete_everything' requires approval
        (reason: default_deny; approval_id=2).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point your agent at the proxy instead of the server, write a policy file, and an agent you don't control is suddenly governed by rules you do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second installation: inside LiteLLM itself.&lt;/strong&gt; LiteLLM loads custom guardrails and calls &lt;code&gt;async_pre_call_hook&lt;/code&gt; before every call — including, usefully, &lt;code&gt;call_type="call_mcp_tool"&lt;/code&gt; for its own MCP gateway. That hook is a ready-made enforcement point, so the adapter is small: build an &lt;code&gt;ActionRequest&lt;/code&gt; from the call, consult the engine, obey.&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;class&lt;/span&gt; &lt;span class="nc"&gt;onedoorGuardrail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CustomGuardrail&lt;/span&gt;&lt;span class="p"&gt;):&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;async_pre_call_hook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_api_key_dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_type&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;call_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;call_mcp_tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mcp.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arguments&lt;/span&gt;&lt;span class="sh"&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;elif&lt;/span&gt; &lt;span class="n"&gt;call_type&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completion&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;acompletion&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llm.completion&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&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;data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running the self-test against the real LiteLLM class, no proxy required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;completion     gpt-4o-mini         -&amp;gt; ok    (permitted, audited)
completion     gpt-4o-experimental -&amp;gt; BLOCK denied (bounds — model not in whitelist)
call_mcp_tool  get_weather         -&amp;gt; ok    (permitted, audited)
call_mcp_tool  send_payment        -&amp;gt; BLOCK requires human approval (approval_id=1)
call_mcp_tool  send_payment €5000  -&amp;gt; BLOCK denied (amount_eur=5000 above max 500.0)
call_mcp_tool  delete_everything   -&amp;gt; BLOCK requires approval (default_deny)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six lines, and the gateway now has the four decisions it was missing: defer with an approval id, value-level bounds, default-deny for unlisted tools, and an audit row with a reason for every verdict — on top of the ACLs it already does well. The two systems compose: LiteLLM's key/team hierarchy decides &lt;em&gt;who&lt;br&gt;
may ask&lt;/em&gt;; the engine decides &lt;em&gt;what may happen&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Take the pattern, not necessarily the code
&lt;/h2&gt;

&lt;p&gt;The engine is open source (Apache-2.0) — SQLite, single process, deliberately boring; the 58-test suite is the release blocker, one demo walks every mechanism end to end with zero external dependencies, a second drives the MCP proxy from an agent's-eye view, and the LiteLLM adapter self-tests without a&lt;br&gt;
proxy. But the code is the smaller half. If you're building anything agentic, the portable part is the checklist, in order:&lt;/p&gt;

&lt;p&gt;One door. Default-deny for the unknown. No undo, no autonomy. Kill switch before policy. Bounds before any human sees a proposal. Rehearsal before budget. Caps reserved race-free. Two-phase execution. Append-only audit.&lt;/p&gt;

&lt;p&gt;The model proposes. The policy layer disposes. Everything else is&lt;br&gt;
implementation.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The engine: &lt;a href="https://github.com/shamiksaharcciit-oss/onedoor" rel="noopener noreferrer"&gt;https://github.com/shamiksaharcciit-oss/onedoor&lt;/a&gt;. It came out of a personal control plane that has run my home since July 2026; the domain modules stayed home, the engine and its tests are public. If you're building agent governance and see something wrong or missing, the issue tracker is&lt;br&gt;
open — that's what it's for.&lt;/em&gt;&lt;/p&gt;

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