<?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: Humberto Villanueva</title>
    <description>The latest articles on DEV Community by Humberto Villanueva (@humbertovillanueva).</description>
    <link>https://dev.to/humbertovillanueva</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%2F4160548%2Fa42bb9d4-db59-41ca-9eaf-01d59f79fe9c.jpg</url>
      <title>DEV Community: Humberto Villanueva</title>
      <link>https://dev.to/humbertovillanueva</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/humbertovillanueva"/>
    <language>en</language>
    <item>
      <title>A Policy Kernel, Not a Prompt</title>
      <dc:creator>Humberto Villanueva</dc:creator>
      <pubDate>Thu, 15 Oct 2026 19:23:40 +0000</pubDate>
      <link>https://dev.to/humbertovillanueva/a-policy-kernel-not-a-prompt-5ch3</link>
      <guid>https://dev.to/humbertovillanueva/a-policy-kernel-not-a-prompt-5ch3</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://humbertovillanueva.dev/writing/a-policy-kernel-not-a-prompt" rel="noopener noreferrer"&gt;humbertovillanueva.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;An agent that runs all night with access to my email, my files and my repositories has to answer one question before anything else: what exactly can it touch? If the answer is a paragraph in the prompt, the honest reply is "whatever it decides." A model can be talked out of an instruction. It cannot be talked past a function that never calls the tool.&lt;/p&gt;

&lt;p&gt;That is the whole idea behind the policy kernel in &lt;a href="https://garuagents.com/" rel="noopener noreferrer"&gt;Garu&lt;/a&gt;, the open-source runtime I built for always-on agents. These are the decisions that shaped it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules live in the agent's file, and the kernel reads them
&lt;/h2&gt;

&lt;p&gt;An agent is one YAML file. Next to its model, its tools and its schedule sits a list of rules. Each rule names a tool, optionally a condition on the arguments, and one of three actions: allow, ask, or block. This is a trimmed version of the rules for Bea, the agent that sorts my inbox every weekday morning:&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;policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tool&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gmail.search_threads&lt;/span&gt;      &lt;span class="c1"&gt;# reading: always fine&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;allow&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tool&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gmail.label_thread&lt;/span&gt;        &lt;span class="c1"&gt;# filing: one click undoes it&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;allow&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tool&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gmail.create_draft&lt;/span&gt;        &lt;span class="c1"&gt;# a reply to an address nobody reads&lt;/span&gt;
    &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;matches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no-?reply|notifications?"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;ignoreCase&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt; &lt;span class="pi"&gt;}&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;block&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tool&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gmail.create_draft&lt;/span&gt;        &lt;span class="c1"&gt;# words in my name: I see each one first&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;ask&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tool&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&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;block&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every tool call the model wants to make goes to the kernel first. The kernel reads the list top to bottom, and the first rule that matches decides. The model never sees this list. It only sees the result: the tool ran, the tool is waiting for a person, or the tool was refused.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nothing matching means ask
&lt;/h2&gt;

&lt;p&gt;The default matters more than any single rule. If no rule matches a call, the kernel does not allow it quietly and it does not refuse it quietly. It asks. A card appears in the inbox and on my phone, and the run waits. If nobody answers within thirty minutes, the answer is no.&lt;/p&gt;

&lt;p&gt;That last part took me a while to accept. An unattended agent that gets a yes by default is not unattended; it is unsupervised. Silence has to be a deny, even when the thing it was asking for was probably fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hide what can never be allowed
&lt;/h2&gt;

&lt;p&gt;If a rule blocks a tool for every possible argument, the model should not know the tool exists. Garu removes it from the list the model is offered. Rook, the agent that watches my repository through GitHub's hosted server, is offered three of that server's forty-six tools. It cannot waste a turn trying to open a pull request, and there is nothing to inject a request into.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Engineering rule:&lt;/strong&gt; a rule the model cannot see is a rule it cannot argue with. Hide the tool, not just the permission.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Arguments are the real boundary
&lt;/h2&gt;

&lt;p&gt;Tool names are too coarse. "Write a file" is harmless in the agent's own folder and dangerous everywhere else. So rules match on arguments too: a write is allowed when the path matches &lt;code&gt;briefs/&lt;/code&gt; followed by today's date, a fetch is allowed for four named hosts, a draft is blocked when the recipient looks like an automated sender. Tomay, the agent that writes my morning brief, has exactly one write rule and never pauses, because the one thing it may write is the one thing it does.&lt;/p&gt;

&lt;p&gt;The kernel also says why. Each rule can carry a reason, and the reason is what the inbox card shows and what the model reads when a call is refused. "Blocked: a reply to an automated sender goes nowhere" teaches more than a bare error.&lt;/p&gt;

&lt;h2&gt;
  
  
  A sandbox for the rest
&lt;/h2&gt;

&lt;p&gt;Policy decides whether a tool runs. It does not decide what a tool server can reach while it runs. For that, each tool server can run in its own container: no network, a read-only root, only the agent's workspace mounted, destroyed after the run. The policy and the sandbox answer different questions, and an agent I would leave alone overnight needs both.&lt;/p&gt;

&lt;h2&gt;
  
  
  What none of this stops
&lt;/h2&gt;

&lt;p&gt;The kernel stops a blocked tool. It does not stop a model from choosing a bad argument for a tool it is allowed to use. Text in an email or a web page can still talk the model into something, and if that something is inside an allowed rule, it happens. The defense is the shape of the rules: allow reads, allow writes to the agent's own folder, and make everything that leaves the machine ask. Narrow allows are the design, not a workaround.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would build first
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Put the decision in code between the model and the tools, never in the prompt.&lt;/li&gt;
&lt;li&gt;Make "nothing matched" mean ask, and make an unanswered ask a no.&lt;/li&gt;
&lt;li&gt;Hide every tool the rules could never allow.&lt;/li&gt;
&lt;li&gt;Match on arguments, and attach a reason to every rule.&lt;/li&gt;
&lt;li&gt;Record every decision, so a 3 a.m. action can be replayed in the morning.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;The test I keep applying is simple: would I let this run while I sleep? With the rules in code, the record on disk and a cost cap on every run, the answer became yes. That is the only feature that matters.&lt;/p&gt;




&lt;p&gt;Garu is an independent open-source project under the Apache-2.0 license, unrelated to my employer. Everything in this note comes from its public repository: &lt;a href="https://github.com/humbertovillanueva/garu" rel="noopener noreferrer"&gt;github.com/humbertovillanueva/garu&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>security</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Make Document Pipelines Fail Loudly</title>
      <dc:creator>Humberto Villanueva</dc:creator>
      <pubDate>Sat, 03 Oct 2026 21:42:47 +0000</pubDate>
      <link>https://dev.to/humbertovillanueva/make-document-pipelines-fail-loudly-24ie</link>
      <guid>https://dev.to/humbertovillanueva/make-document-pipelines-fail-loudly-24ie</guid>
      <description>&lt;p&gt;A document pipeline takes PDFs, drawings, and reports, pulls out text and metadata, and makes them searchable. When it crashes, someone notices. When it quietly skips a page, drops a field, or gives up on a file without saying so, nobody notices until a person asks a question and gets a confident, incomplete answer.&lt;/p&gt;

&lt;p&gt;Most of my reliability work has been about that second kind of failure. These are the patterns I keep coming back to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give failures a small, honest vocabulary
&lt;/h2&gt;

&lt;p&gt;“Something went wrong” is not a state. Every failed document should land in one of a few named states, and the most important split is simple: can this fix itself, or does a person need to act?&lt;/p&gt;

&lt;p&gt;A rate limit from an AI provider is retryable. A missing API key is not, and retrying it a hundred times just hides the real problem behind noise. If the code that records a failure cannot tell those apart, the system will eventually retry the unfixable and give up on the fixable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ingested        everything extracted
provisional     usable, but something was missing
retrying        temporary problem, will try again
failed          a person needs to act (with a reason)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Show the reason where people look
&lt;/h2&gt;

&lt;p&gt;A failure that only exists in a server log is invisible to the people who depend on the data. The interface should say which documents are incomplete and why, in plain words. A warning badge with an empty tooltip is worse than no badge: it tells people something is wrong and then refuses to explain.&lt;/p&gt;

&lt;p&gt;One subtle trap: if health reasons are only ever appended, an old problem can keep showing after it has been fixed. Each check should own its reasons and replace them on every run, so the badge always reflects the current state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retries have to survive a restart
&lt;/h2&gt;

&lt;p&gt;Retry logic usually works fine until the server restarts. Then work that was waiting for a retry is simply forgotten, or it comes back with its attempt counter reset to one, so the retry limit never kicks in.&lt;/p&gt;

&lt;p&gt;Persist the retry state with the document: how many attempts, why it is waiting, and when to try next. On startup, look for anything that was mid-retry and pick it back up. Then add a check for documents stuck in limbo, neither finished nor failed, because that is exactly where silent losses hide.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ENGINEERING RULE&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If a document can enter a state, there should be a way to find every document in that state. “Stuck” is a state too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch the boring code paths
&lt;/h2&gt;

&lt;p&gt;Some of the worst bugs I have found were not in the clever parts. They were in plumbing: an update function with an allow-list of fields that silently dropped a new one, or a log message that said a step succeeded and failed at the same time. Plumbing like that deserves small, focused tests, because nobody reads it closely once it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Characterize before you fix
&lt;/h2&gt;

&lt;p&gt;When a number looks wrong, my first instinct used to be to change the code. Now I write a test that captures what the code does today, wrong answer included. That test proves the bug exists, shows how big it is, and tells me exactly when my fix changes behavior and when it does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would build first
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Name the failure states and split retryable from permanent.&lt;/li&gt;
&lt;li&gt;Store a human-readable reason with every failure.&lt;/li&gt;
&lt;li&gt;Surface incomplete documents in the interface, not just the logs.&lt;/li&gt;
&lt;li&gt;Persist retry state and resume it on startup.&lt;/li&gt;
&lt;li&gt;Add a query for anything stuck between states.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;A pipeline that fails loudly is less impressive in a demo, because it admits what it could not do. It is far more useful in production, because the people relying on it know which answers they can trust.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://humbertovillanueva.dev/writing/make-document-pipelines-fail-loudly" rel="noopener noreferrer"&gt;humbertovillanueva.dev&lt;/a&gt;. I'm Humberto Villanueva, a software engineer in Utah building web applications, AI integrations, and software for building data. More of my work: &lt;a href="https://humbertovillanueva.dev" rel="noopener noreferrer"&gt;humbertovillanueva.dev&lt;/a&gt; · &lt;a href="https://github.com/humbertovillanueva" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/humberto-villanueva-dev/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article describes general engineering principles from my experience and independent study. It does not disclose proprietary architecture, source code, customer information, or confidential details from kW Engineering.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>testing</category>
      <category>dataengineering</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
