<?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: DVARA</title>
    <description>The latest articles on DEV Community by DVARA (@dvarahq).</description>
    <link>https://dev.to/dvarahq</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%2F3995849%2Ff9318089-ab1a-4327-9a9a-3e825e396885.png</url>
      <title>DEV Community: DVARA</title>
      <link>https://dev.to/dvarahq</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dvarahq"/>
    <language>en</language>
    <item>
      <title>readOnlyHint isn't a permission: blocking a destructive MCP tool by policy</title>
      <dc:creator>DVARA</dc:creator>
      <pubDate>Tue, 27 Oct 2026 10:53:46 +0000</pubDate>
      <link>https://dev.to/dvarahq/readonlyhint-isnt-a-permission-blocking-a-destructive-mcp-tool-by-policy-19nm</link>
      <guid>https://dev.to/dvarahq/readonlyhint-isnt-a-permission-blocking-a-destructive-mcp-tool-by-policy-19nm</guid>
      <description>&lt;p&gt;MCP lets a tool describe itself. &lt;code&gt;readOnlyHint: true&lt;/code&gt; says "I only read". &lt;code&gt;destructiveHint: true&lt;/code&gt; says "I can delete or overwrite something". Clients can use those hints to decide when to ask a person before running a tool.&lt;/p&gt;

&lt;p&gt;Two things go wrong with that in practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The hint is often missing.&lt;/strong&gt; We looked at about 20,000 MCP tools for a &lt;a href="https://dvarahq.com/blog/mcp-security-tool-annotations" rel="noopener noreferrer"&gt;report on MCP tool annotations&lt;/a&gt;. In Docker's MCP Catalog, 69% of tools that delete or overwrite something carry no hint at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The hint can be wrong.&lt;/strong&gt; One server in that catalogue marked 15 delete tools as read-only, "delete ALL integrations" among them. The tools never set a hint, and a framework default filled in &lt;code&gt;readOnlyHint: true&lt;/code&gt; for every one. The vendor fixed it within a week; the catalogue still served the old version three months later.&lt;/p&gt;

&lt;p&gt;The MCP specification already tells clients what to do about this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For trust &amp;amp; safety and security, clients &lt;strong&gt;MUST&lt;/strong&gt; consider tool annotations to be untrusted unless they come from trusted servers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So where should the decision about what an agent may do live? Not in the tool's description of itself. Here's what it looks like to put it somewhere you control instead, with a real run you can reproduce in about five minutes.&lt;/p&gt;

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

&lt;p&gt;The &lt;a href="https://dvarahq.com/docs/mcp-quickstart" rel="noopener noreferrer"&gt;DVARA MCP quickstart&lt;/a&gt; starts a gateway, its console and a small Spring AI MCP server with three tools:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_order_status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reads an order's status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lookup_customer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Returns a customer record, including an email, phone and card number&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;delete_customer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deletes a customer record. Irreversible.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Your agent talks to the gateway, not to the server. The gateway lists the server's tools to the agent as &lt;code&gt;demo-tools__&amp;lt;tool&amp;gt;&lt;/code&gt; and applies the workspace's rules to every call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--depth&lt;/span&gt; 1 https://github.com/dvarahq/dvara-examples.git
&lt;span class="nb"&gt;cd &lt;/span&gt;dvara-examples/docker-compose/mcp-quickstart
./start.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You need Docker and JBang. No licence key and no AI provider account.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule
&lt;/h2&gt;

&lt;p&gt;The quickstart loads one policy into the workspace. This is all of it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;
&lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deny-delete&lt;/span&gt;
    &lt;span class="na"&gt;conditions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;mcp_tool&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;denylist&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;delete_customer&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DENY&lt;/span&gt;
    &lt;span class="na"&gt;deny_message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delete_customer&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;irreversible&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;not&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;allowed&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;this&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;workspace"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what it doesn't depend on: the tool's annotations. If &lt;code&gt;delete_customer&lt;/code&gt; claimed to be read-only, or said nothing at all, the rule would apply just the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  The call
&lt;/h2&gt;

&lt;p&gt;Ask the gateway to run the tool, the way any MCP client would. Replace &lt;code&gt;&amp;lt;your-api-key&amp;gt;&lt;/code&gt; with &lt;code&gt;DEMO_API_KEY&lt;/code&gt; from the quickstart's &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:8080/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;your-api-key&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept: application/json, text/event-stream"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"demo-tools__delete_customer","arguments":{"customerId":"C-42"}}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gateway refuses it before it reaches the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;-32001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"delete_customer is irreversible and not allowed in this workspace"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"dvara_code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"MCP_POLICY_DENIED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"http_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"trace_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"3054f7e845e4437ea9ec3fb704fe9a0e"&lt;/span&gt;&lt;span class="p"&gt;}}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The demo server writes a line to its log whenever &lt;code&gt;delete_customer&lt;/code&gt; actually runs. After this call, there's no such line: the server never received it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The record
&lt;/h2&gt;

&lt;p&gt;Open the console at &lt;code&gt;http://localhost:8090&lt;/code&gt; and go to &lt;strong&gt;MCP → Tool Calls&lt;/strong&gt;. The refused call is there with status &lt;code&gt;403&lt;/code&gt;, next to the allowed &lt;code&gt;get_order_status&lt;/code&gt; and the &lt;code&gt;lookup_customer&lt;/code&gt; call whose result had its email, phone and card number redacted.&lt;/p&gt;

&lt;p&gt;That record is the other half of the point. When someone asks what an agent did, or tried to do, the answer doesn't depend on each server's logging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adapting it to your own tools
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deny by name.&lt;/strong&gt; Add the tools that should never run unattended to the &lt;code&gt;denylist&lt;/code&gt;. Start with the ones whose names say delete, drop, revoke, transfer or send.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hold for a person instead of denying.&lt;/strong&gt; Some actions should happen, just not without a human saying yes. DVARA can hold a tool call for approval; the &lt;a href="https://dvarahq.com/docs/native-mcp" rel="noopener noreferrer"&gt;native MCP endpoint docs&lt;/a&gt; cover how.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat a missing hint as unknown.&lt;/strong&gt; If a tool you haven't classified shows up, the safe default is to ask, not to assume it only reads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin your servers.&lt;/strong&gt; Catalogues can serve an older version than upstream. Know which version you run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Annotations are still worth setting if you write MCP servers: they help every client that does read them. Just don't make them the thing that stands between an agent and an irreversible action.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Data in this post comes from our &lt;a href="https://dvarahq.com/blog/mcp-security-tool-annotations" rel="noopener noreferrer"&gt;report on MCP tool annotations&lt;/a&gt;, which has the full method. Tool data: Glama (glama.ai); Docker MCP Catalog data is MIT-licensed.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>security</category>
      <category>ai</category>
      <category>agents</category>
    </item>
    <item>
      <title>Routing Is Not Failover: A Practical Multi-Provider LLM Pattern</title>
      <dc:creator>DVARA</dc:creator>
      <pubDate>Mon, 28 Sep 2026 13:08:30 +0000</pubDate>
      <link>https://dev.to/dvarahq/routing-is-not-failover-a-practical-multi-provider-llm-pattern-2e1p</link>
      <guid>https://dev.to/dvarahq/routing-is-not-failover-a-practical-multi-provider-llm-pattern-2e1p</guid>
      <description>&lt;p&gt;Adding a second LLM provider does not automatically make an application resilient.&lt;/p&gt;

&lt;p&gt;The common mistake is treating provider routing, retry and failover as one decision. They answer different questions at different points in a request. When those boundaries are unclear, teams can send traffic to an incompatible provider, bypass controls or struggle to explain why a request went where it did.&lt;/p&gt;

&lt;p&gt;A safer multi-provider design keeps the stages explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
  → governance checks
  → capability filter
  → routing strategy
  → selected provider

After an eligible failure:
  selected provider
  → retry same provider
  → compatible failover
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Govern first, then route
&lt;/h2&gt;

&lt;p&gt;Provider selection should not happen before policy enforcement. The request should first be associated with its workspace and credentials, then pass through the required policy, PII, guardrail and rate-limit checks.&lt;/p&gt;

&lt;p&gt;Capability filtering comes next. A provider may be healthy but still unable to satisfy a request involving structured output, tool calling, vision or another required feature. Filtering before routing ensures that the strategy operates only on eligible providers.&lt;/p&gt;

&lt;p&gt;Only then should the gateway select an upstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with a strategy you can verify
&lt;/h2&gt;

&lt;p&gt;Static weighted routing is useful when two compatible providers should receive a controlled traffic split. In &lt;a href="https://github.com/dvarahq/dvara" rel="noopener noreferrer"&gt;DVARA Open Source 1.8.0&lt;/a&gt;, a route can be expressed in &lt;code&gt;gateway.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openai&lt;/span&gt;
    &lt;span class="na"&gt;api_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${OPENAI_API_KEY}&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure-openai&lt;/span&gt;
    &lt;span class="na"&gt;api_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${AZURE_OPENAI_API_KEY}&lt;/span&gt;
    &lt;span class="na"&gt;base_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${AZURE_OPENAI_BASE_URL}&lt;/span&gt;

&lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;weighted-gpt&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt*"&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;weighted&lt;/span&gt;
    &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openai&lt;/span&gt;
        &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure-openai&lt;/span&gt;
        &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both providers must accept the model identifier sent by the application. Routing selects a provider; it does not translate one provider's model name into another provider's deployment name.&lt;/p&gt;

&lt;p&gt;Open Source reads this file at startup, so a routing change requires a Gateway restart. After restarting, verify the result over a meaningful sample of requests. A few calls are not enough to judge a probabilistic distribution. The response trace and structured access log's &lt;code&gt;provider&lt;/code&gt; field show which upstream handled each request.&lt;/p&gt;

&lt;p&gt;DVARA Open Source 1.8.0 also supports model-prefix, round-robin and canary routing. Latency-aware, cost-aware, geo-aware and intelligent routing are Enterprise capabilities, not Open Source strategies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep recovery separate
&lt;/h2&gt;

&lt;p&gt;Routing asks: &lt;strong&gt;Which eligible provider should receive this request first?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Retry asks: &lt;strong&gt;Should the same provider receive another attempt after a retryable failure?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Failover asks: &lt;strong&gt;Can a compatible alternative safely take over after eligible retries or provider unavailability?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That last word—compatible—is important. A text request succeeding against a fallback does not prove that structured output, tool calling, streaming or vision will also work. Test each request shape and make failure explicit when no compatible provider remains.&lt;/p&gt;

&lt;p&gt;Keeping recovery separate also makes the evidence easier to interpret. A trace should show the original routing decision, the provider that received the request, and whether retry or fallback occurred afterward.&lt;/p&gt;

&lt;h2&gt;
  
  
  One path, one explanation
&lt;/h2&gt;

&lt;p&gt;The practical goal is not merely to distribute traffic. It is to preserve the same governance and evidence path regardless of which provider is selected.&lt;/p&gt;

&lt;p&gt;Start with one model family and one operational goal: an even split, a controlled canary or a static weight. Confirm that policy and capability checks still apply, simulate an unavailable provider, and inspect the resulting trace before expanding the route.&lt;/p&gt;

&lt;p&gt;For the complete configuration, verification steps and strategy boundaries, read the &lt;strong&gt;&lt;a href="https://dvarahq.com/blog/multi-provider-llm-routing?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=seo_refresh_q4_2026&amp;amp;utm_content=multi_provider_routing_excerpt" rel="noopener noreferrer"&gt;practical guide to multi-provider LLM routing&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>architecture</category>
      <category>opensource</category>
    </item>
    <item>
      <title>LLM Policy as Code: Version-Controlled Governance for Model and Agent Access</title>
      <dc:creator>DVARA</dc:creator>
      <pubDate>Tue, 07 Jul 2026 09:32:44 +0000</pubDate>
      <link>https://dev.to/dvarahq/llm-policy-as-code-version-controlled-governance-for-model-and-agent-access-50fl</link>
      <guid>https://dev.to/dvarahq/llm-policy-as-code-version-controlled-governance-for-model-and-agent-access-50fl</guid>
      <description>&lt;p&gt;Ask a team "which models is your application allowed to call, and under what conditions?" and the honest answer is usually &lt;em&gt;"let me check the code."&lt;/em&gt; The rules — which models are approved, which tools an agent may invoke, what happens when a request is too large or comes from the wrong region — are scattered across &lt;code&gt;if&lt;/code&gt; statements in a dozen services. No one can review them in one place, no one can test a change safely, and no one can say what a rule &lt;em&gt;would have done&lt;/em&gt; before it ships.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LLM policy as code&lt;/strong&gt; fixes that the same way infrastructure as code fixed server configuration: move the rules out of application code and into a declarative, version-controlled language that a single control point enforces on every call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is LLM policy as code?
&lt;/h2&gt;

&lt;p&gt;LLM policy as code is the practice of expressing your governance rules — who can call which models and tools, under which conditions — in a declarative, version-controlled format, and enforcing them centrally at a gateway on every LLM and agent (MCP) request, instead of hard-coding them in each application.&lt;/p&gt;

&lt;p&gt;It's the same idea behind Terraform for infrastructure, Open Policy Agent for authorization, and Kubernetes admission control for clusters: the policy is a reviewable artifact, not tribal knowledge. The difference is the domain — an LLM policy matches on &lt;em&gt;AI-native&lt;/em&gt; attributes (model, token budget, requested tools, data-residency region) that a general-purpose API gateway or IAM system can't see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why governance in application code fails
&lt;/h2&gt;

&lt;p&gt;Rules embedded in app code have four problems, and they compound:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You can't review them.&lt;/strong&gt; "What's allowed?" has no single answer — you'd have to read every repo that calls a model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can't test them.&lt;/strong&gt; Changing a rule means a deploy and hoping nothing breaks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can't roll them back.&lt;/strong&gt; A bad rule is another deploy to undo, under pressure, with no record of the previous state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can't audit them.&lt;/strong&gt; There's no trail of who changed which rule, when, or what decision it produced.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Policy as code turns all four from "read the source" into "read the policy" — a diffable, testable, reversible, auditable object.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an LLM policy looks like
&lt;/h2&gt;

&lt;p&gt;A policy is a small YAML document: a set of &lt;strong&gt;rules&lt;/strong&gt;, each with an &lt;code&gt;id&lt;/code&gt;, a &lt;code&gt;priority&lt;/code&gt; (lower number wins), a &lt;code&gt;conditions&lt;/code&gt; block that matches request attributes, and exactly one &lt;code&gt;action&lt;/code&gt;. Here's a model allowlist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;
&lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;only-approved-models&lt;/span&gt;
    &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
    &lt;span class="na"&gt;conditions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;allowlist&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;gpt-4o&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;gpt-4o-mini&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;claude-sonnet-4-5&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DENY&lt;/span&gt;
    &lt;span class="na"&gt;deny_message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Model&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;not&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;on&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;approved&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;list"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rules evaluate in priority order. The first matching &lt;code&gt;DENY&lt;/code&gt; returns immediately; a softer &lt;code&gt;WARN_AGENT&lt;/code&gt; action collects a warning and lets the call continue.&lt;/p&gt;

&lt;p&gt;The conditions you can match on are the ones that matter for AI traffic: &lt;strong&gt;&lt;code&gt;model&lt;/code&gt;&lt;/strong&gt; (allow/denylist), &lt;strong&gt;&lt;code&gt;max_tokens&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;tools&lt;/code&gt;&lt;/strong&gt; and MCP server/tool/args, &lt;strong&gt;&lt;code&gt;data_residency&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;time_of_day&lt;/code&gt;&lt;/strong&gt;, and &lt;strong&gt;&lt;code&gt;budget&lt;/code&gt; utilization&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  When the simple form isn't enough
&lt;/h3&gt;

&lt;p&gt;For rules that need &lt;strong&gt;OR&lt;/strong&gt;, &lt;strong&gt;NOT&lt;/strong&gt;, or arithmetic across fields, a rule can carry a CEL (Common Expression Language) &lt;code&gt;expression:&lt;/code&gt; instead of &lt;code&gt;conditions:&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;
&lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gpt4o-outside-eu-or-large-requests&lt;/span&gt;
    &lt;span class="na"&gt;expression&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;request.model == "gpt-4o" &amp;amp;&amp;amp;&lt;/span&gt;
      &lt;span class="s"&gt;(context.region != "EU" || request.message_count &amp;gt;= 50)&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DENY&lt;/span&gt;
    &lt;span class="na"&gt;deny_message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;restricted&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;outside&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;EU&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;or&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;50&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;messages"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CEL is the same sandboxed expression language Kubernetes admission policies, Envoy, and Cilium use — deterministic and type-checked at submit time. One expression replaces three rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lifecycle is what makes it safe to change
&lt;/h2&gt;

&lt;p&gt;Writing rules as code is only half the value; the other half is changing them without fear:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Author&lt;/strong&gt; — the policy starts as a &lt;strong&gt;Draft&lt;/strong&gt;, affecting nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dry-run&lt;/strong&gt; — simulate it against a real request and see the exact decision before it touches live traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shadow&lt;/strong&gt; — run the candidate in production &lt;em&gt;in parallel&lt;/em&gt;, recording what it &lt;em&gt;would&lt;/em&gt; have done with divergence stats, while changing nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Promote&lt;/strong&gt; — activate it; conflict detection surfaces contradictory rules, and it hot-reloads across the fleet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version &amp;amp; roll back&lt;/strong&gt; — every change snapshots a version; revert instantly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Shadow mode is the part teams underrate — the difference between "we think this rule is safe" and "we watched it run against a week of real traffic."&lt;/p&gt;

&lt;h2&gt;
  
  
  Global and tenant scope, audited on every call
&lt;/h2&gt;

&lt;p&gt;Policies apply platform-wide and per-tenant; both are evaluated on every request, and the decision — allowed, denied, or warned, and by which rule — is recorded on every LLM and MCP call. That record is what turns "we have policies" into "here's proof of what our policies did."&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it runs
&lt;/h2&gt;

&lt;p&gt;The rules are enforced at the gateway in front of your model and tool calls — one control point every request already passes through, with full visibility into the AI semantics (model, tokens, tools, region, budget). A denied request never leaves your perimeter.&lt;/p&gt;

&lt;p&gt;That's the core of DVARA's approach: &lt;strong&gt;&lt;a href="https://dvarahq.com/policy-as-code-llm" rel="noopener noreferrer"&gt;Policy-as-Code for LLM and agent traffic&lt;/a&gt;&lt;/strong&gt; as a first-class, version-controlled surface — author it in YAML, dry-run and shadow-test it, promote with conflict detection, roll back in one click. Read the full pillar guide → &lt;strong&gt;&lt;a href="https://dvarahq.com/policy-as-code-llm" rel="noopener noreferrer"&gt;https://dvarahq.com/policy-as-code-llm&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

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