<?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>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>
