<?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: Steve Gonzalez</title>
    <description>The latest articles on DEV Community by Steve Gonzalez (@goweft).</description>
    <link>https://dev.to/goweft</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%2F3839366%2F25073b27-58cf-4933-975b-920278b4336f.png</url>
      <title>DEV Community: Steve Gonzalez</title>
      <link>https://dev.to/goweft</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/goweft"/>
    <language>en</language>
    <item>
      <title>The Security Gap in MCP Tool Servers (And What I Built to Fix It)</title>
      <dc:creator>Steve Gonzalez</dc:creator>
      <pubDate>Mon, 10 Aug 2026 03:33:05 +0000</pubDate>
      <link>https://dev.to/goweft/the-security-gap-in-mcp-tool-servers-and-what-i-built-to-fix-it-5c</link>
      <guid>https://dev.to/goweft/the-security-gap-in-mcp-tool-servers-and-what-i-built-to-fix-it-5c</guid>
      <description>&lt;p&gt;MCP (Model Context Protocol) is how AI agents connect to tools. Claude Desktop uses it, Cursor uses it, and thousands of developers are building MCP servers to give AI access to their APIs, databases, and infrastructure.&lt;/p&gt;

&lt;p&gt;There's one problem: &lt;strong&gt;MCP has no security model.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The protocol defines how a client talks to a server, but says nothing about what that server is allowed to do. No authentication between client and server. No authorization on which tools can be called. No audit trail of what happened. The spec assumes you'll handle all of that yourself.&lt;/p&gt;

&lt;p&gt;Most people don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Goes Wrong
&lt;/h2&gt;

&lt;p&gt;I run a self-hosted server with Prometheus, Grafana, Ollama, Gitea, and a handful of other services. I wanted Claude Desktop to query all of them through MCP. The standard approach is to write a Python FastMCP server for each one — a few dozen lines per service, hardcode the API key, register the tools, done.&lt;/p&gt;

&lt;p&gt;That works until you think about what you've actually built:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every MCP server has full access to whatever its process can reach.&lt;/strong&gt; Your Prometheus tool can also hit your Grafana API, your Gitea API, and anything else on localhost. There's no scoping.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API keys live in environment variables or config files.&lt;/strong&gt; If you have 9 MCP servers, you have 9 places where credentials sit in plaintext with no access policy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nothing is logged.&lt;/strong&gt; If Claude calls a tool that restarts a service or deletes data, there's no record of which tool was called, with what parameters, by which agent, at what time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There's no concept of read-only vs. write.&lt;/strong&gt; A tool either exists or it doesn't. MCP doesn't know that &lt;code&gt;query_prometheus&lt;/code&gt; is safe to call freely but &lt;code&gt;restart_service&lt;/code&gt; should require approval.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool composition creates emergent risks.&lt;/strong&gt; When Claude has access to multiple MCP servers, it can chain calls across them. Server A reads sensitive data, Server B posts to an external API — Claude could combine them in ways neither server was designed for.&lt;/p&gt;

&lt;p&gt;These aren't theoretical risks. During development, I declared an agent as read-only (Trust Tier 1) but gave it a tool that used HTTP POST. The system I built caught it — blocked the call, logged a trust violation, and forced me to either fix the config or explicitly upgrade the trust level. Without that enforcement, the tool would have silently worked and I'd never have known my security model was wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/goweft/heddle" rel="noopener noreferrer"&gt;Heddle&lt;/a&gt; is a runtime that sits between your YAML config and the MCP protocol. You define your tools in a config file, and Heddle validates, secures, and serves them — with policy enforcement on every call.&lt;/p&gt;

&lt;p&gt;Here's a complete tool server for Prometheus:&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;agent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prometheus-bridge&lt;/span&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.0.0"&lt;/span&gt;
  &lt;span class="na"&gt;exposes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;query_prometheus&lt;/span&gt;
      &lt;span class="na"&gt;access&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Run&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;PromQL&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;query"&lt;/span&gt;
      &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;string&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;required&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;get_alerts&lt;/span&gt;
      &lt;span class="na"&gt;access&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;List&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;active&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Prometheus&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;alerts"&lt;/span&gt;
  &lt;span class="na"&gt;http_bridge&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_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;query_prometheus&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GET&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:9090/api/v1/query"&lt;/span&gt;
      &lt;span class="na"&gt;query_params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;query&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_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;get_alerts&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GET&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:9090/api/v1/alerts"&lt;/span&gt;
  &lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;trust_tier&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;heddle run agents/prometheus-bridge.yaml&lt;/code&gt; and Claude can query Prometheus in natural language. But every call goes through a six-layer dispatch pipeline before it reaches the API:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate limiting&lt;/strong&gt; → &lt;strong&gt;Access mode check&lt;/strong&gt; → &lt;strong&gt;Escalation rules&lt;/strong&gt; → &lt;strong&gt;Trust tier enforcement&lt;/strong&gt; → &lt;strong&gt;Input validation&lt;/strong&gt; → &lt;strong&gt;HTTP bridge execution&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Each layer can independently block the call and log why.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Security Controls
&lt;/h2&gt;

&lt;p&gt;The dispatch pipeline enforces these controls on every tool call:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trust Tiers (T1—T4).&lt;/strong&gt; Each config declares a trust level. T1 (observer) can only use GET — any POST/PUT/DELETE is blocked at runtime, not just warned. T2 (worker) allows scoped writes. T3 (operator) allows cross-agent invocation. T4 (privileged) requires human approval. I caught a real misconfiguration with this — a T1 agent tried to POST and the enforcer blocked it before the request ever left the process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Access Mode Annotations.&lt;/strong&gt; Every tool is declared as &lt;code&gt;access: read&lt;/code&gt; or &lt;code&gt;access: write&lt;/code&gt;. T1 configs with write tools are rejected at load time — before the server even starts. This is the schema-level version of least privilege.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Credential Broker.&lt;/strong&gt; API keys are stored in &lt;code&gt;~/.heddle/secrets.json&lt;/code&gt; with per-config access policies. Configs reference them as &lt;code&gt;{{secret:prometheus-token}}&lt;/code&gt; — resolved at runtime, never written to the YAML file. A config can only access secrets it's been explicitly granted. Unauthorized access is denied, logged, and the call aborts before any HTTP request is constructed — fail-closed. (It didn't start out that way: the launch version handed back a placeholder string and let the request proceed anyway. An external security review caught it. More on that below.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Escalation Rules.&lt;/strong&gt; Declarative conditions that hold a tool call for review instead of executing it. For example, my VRAM orchestrator has a rule that holds any &lt;code&gt;smart_load&lt;/code&gt; call if the model name contains "27b" — because loading a 27-billion parameter model consumes most of my 24GB GPU memory. The rule triggers, the call is held, and the audit log records why.&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;escalation_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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;large-model-load&lt;/span&gt;
    &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Loading&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&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;that&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;will&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;consume&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;most&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&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;24GB&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;VRAM"&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;smart_load"&lt;/span&gt;
    &lt;span class="na"&gt;param_contains&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;model_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;27b"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Input Validation.&lt;/strong&gt; Type checking, length limits, and injection pattern detection on every parameter. The validator catches shell injection (&lt;code&gt;; rm -rf /&lt;/code&gt;), SQL injection (&lt;code&gt;' OR 1=1&lt;/code&gt;), path traversal (&lt;code&gt;../../etc/passwd&lt;/code&gt;), and LLM prompt injection (&lt;code&gt;ignore previous instructions&lt;/code&gt;). In strict mode, these are blocked. In permissive mode, they're logged and passed through.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hash-Chained Audit Log.&lt;/strong&gt; Every tool call, trust violation, credential access, and escalation hold is logged as a JSON Lines entry. Each entry includes a SHA-256 hash of the previous entry — if anyone modifies or deletes a log entry, the chain breaks and verification fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Config Signing.&lt;/strong&gt; All YAML configs are signed with HMAC-SHA256. If a config is modified after signing, the runtime detects the tampering. AI-generated configs (from Heddle's natural language generator) are automatically quarantined in a staging directory until explicitly promoted.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Looks Like Running
&lt;/h2&gt;

&lt;p&gt;I'm currently running 46 tools from 9 active configs through a single MCP connection to Claude Desktop (11 configs total; two are excluded for incompatible transports). The configs cover Prometheus, Grafana, Ollama, Gitea, an RSS aggregator, a RAG search API, a multi-model AI platform bridge, a GPU VRAM orchestrator, and a daily operations briefing agent.&lt;/p&gt;

&lt;p&gt;Every one of those 46 tools goes through the same dispatch pipeline. The Prometheus tools are T1 (read-only, five tools). The Ollama bridge is T2 (can POST for text generation). The VRAM orchestrator is T3 (can invoke other agents, has escalation rules on destructive operations).&lt;/p&gt;

&lt;p&gt;The trust tiers aren't just labels — they're enforced. A T1 config physically cannot make a POST request, even if the HTTP bridge URL is correct and the API would accept it. The enforcer blocks it before the request is constructed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then I Invited the Audit
&lt;/h2&gt;

&lt;p&gt;Shipping a security tool comes with an obligation: you have to let people try to break it. After v0.2.0 I handed the repo to two independent AI-assisted security reviews — different frontier models, no shared context — plus a manual pass of my own, and asked for the worst.&lt;/p&gt;

&lt;p&gt;They delivered. Four findings, all real:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The most-used path bypassed the pipeline.&lt;/strong&gt; The custom stdio handlers — the exact transport Claude Desktop uses — dispatched tool calls directly, skipping the six-layer pipeline entirely. The security model was airtight on the HTTP server and absent on the path that mattered most. Every execution path now routes through a single &lt;code&gt;ToolPolicy.guard()&lt;/code&gt;, and 13 adversarial tests exist to prove the bypass stays closed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Credential denial failed open.&lt;/strong&gt; As above: a denied secret produced a placeholder and the request went out anyway. Against a permissive upstream, an unauthenticated call could silently succeed. &lt;code&gt;CredentialDenied&lt;/code&gt; now aborts the call before a request object is even constructed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Redaction had gaps a hash chain makes permanent.&lt;/strong&gt; The audit redactor didn't recurse into nested structures, and it scrubbed URLs by token pattern instead of parsing query parameters by key. That matters more here than in an ordinary log: a hash-chained entry can't be scrubbed after the fact without breaking &lt;code&gt;verify_chain()&lt;/code&gt;. A leaked secret would be tamper-evidently permanent. Redaction now recurses, and URLs are parsed structurally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Handler codegen used &lt;code&gt;exec()&lt;/code&gt;.&lt;/strong&gt; HTTP-bridge handlers were generated as source strings and &lt;code&gt;exec()&lt;/code&gt;'d. No config-influenced string reaches &lt;code&gt;exec()&lt;/code&gt; anymore — handlers are real typed callables built with &lt;code&gt;inspect.Signature&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;All of it shipped as &lt;a href="https://github.com/goweft/heddle/releases/tag/v0.2.1" rel="noopener noreferrer"&gt;v0.2.1, an assurance release&lt;/a&gt;: no new features, every change closes a finding, and the test count went from 237 to 273 almost entirely on regression tests that pin these fixes down.&lt;/p&gt;

&lt;p&gt;The uncomfortable part: the credential fail-open was described in the first draft of this very post as a feature. That's the strongest argument I know for external review — the author is the one person guaranteed to read the design intent instead of the behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Framework Mapping
&lt;/h2&gt;

&lt;p&gt;Every security control maps to at least one industry framework. (OWASP renumbered agentic risks in December 2025 — this table uses the current ASI identifiers from the Top 10 for Agentic Applications.) This matters if you're in an organization that needs to demonstrate compliance, or if you're building a portfolio that shows applied security architecture (which is why I built this):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;th&gt;OWASP Agentic (2026)&lt;/th&gt;
&lt;th&gt;NIST AI RMF&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Trust tiers&lt;/td&gt;
&lt;td&gt;ASI03 Identity &amp;amp; Privilege Abuse&lt;/td&gt;
&lt;td&gt;GV-1.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Credential broker&lt;/td&gt;
&lt;td&gt;ASI03 Identity &amp;amp; Privilege Abuse&lt;/td&gt;
&lt;td&gt;MAP-3.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit logging&lt;/td&gt;
&lt;td&gt;ASI10 Rogue Agents&lt;/td&gt;
&lt;td&gt;MS-2.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input validation&lt;/td&gt;
&lt;td&gt;ASI01 Agent Goal Hijack&lt;/td&gt;
&lt;td&gt;MS-2.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Config signing&lt;/td&gt;
&lt;td&gt;ASI04 Agentic Supply Chain&lt;/td&gt;
&lt;td&gt;GV-6.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Escalation rules&lt;/td&gt;
&lt;td&gt;ASI02 Tool Misuse &amp;amp; Exploitation&lt;/td&gt;
&lt;td&gt;GV-1.3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The full threat model is in the repo at &lt;a href="https://github.com/goweft/heddle/blob/master/docs/threat-model.md" rel="noopener noreferrer"&gt;docs/threat-model.md&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/goweft/heddle.git
&lt;span class="nb"&gt;cd &lt;/span&gt;heddle
python &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;".[dev]"&lt;/span&gt;

&lt;span class="c"&gt;# Try a starter pack&lt;/span&gt;
&lt;span class="nb"&gt;cp &lt;/span&gt;packs/prometheus.yaml agents/
heddle validate agents/prometheus.yaml
heddle run agents/prometheus.yaml &lt;span class="nt"&gt;--port&lt;/span&gt; 8200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Heddle ships with 6 starter packs — Prometheus, Grafana, Gitea/GitHub, Ollama, Sonarr, and Radarr — that you can drop into &lt;code&gt;agents/&lt;/code&gt; and run immediately. All read-only (T1) except Ollama (T2 for text generation).&lt;/p&gt;

&lt;p&gt;Or generate a config from natural language:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;heddle generate &lt;span class="s2"&gt;"agent that wraps the Home Assistant API"&lt;/span&gt; &lt;span class="nt"&gt;--model&lt;/span&gt; qwen3:14b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Works with Claude Desktop, Cursor, and any MCP client that supports stdio transport.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;The next release makes Heddle's own policy exceptions mortal. &lt;a href="https://github.com/goweft/heddle/blob/master/docs/decisions/005-exception-lifecycle.md" rel="noopener noreferrer"&gt;ADR 005&lt;/a&gt; proposes expiry horizons that scale inversely with privilege (a T4 grant lives 30 days, not forever), credential grants bound to config signatures so a re-signed config invalidates its approvals, and &lt;code&gt;revoke_when&lt;/code&gt; predicates that revoke a grant at dispatch time the moment its justifying assumption stops being true. The invariant: every exception carries exactly one collector — a date, a signature pin, a predicate, or a reconciliation pass. A promise nobody can collect on is not a control.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Heddle&lt;/strong&gt; is open source (MIT) at &lt;a href="https://github.com/goweft/heddle" rel="noopener noreferrer"&gt;github.com/goweft/heddle&lt;/a&gt;. 273 tests, 15 security controls, and a threat model mapped to the OWASP Top 10 for Agentic Applications (2026) and NIST AI RMF. If you're exposing APIs to AI agents, I'd like to know what security controls you wish existed.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>security</category>
      <category>python</category>
      <category>ai</category>
    </item>
    <item>
      <title>Seven Agents, Zero Trust: How I Made an Agentic Shell Safe by Design</title>
      <dc:creator>Steve Gonzalez</dc:creator>
      <pubDate>Sun, 09 Aug 2026 22:48:53 +0000</pubDate>
      <link>https://dev.to/goweft/seven-agents-zero-trust-how-i-made-an-agentic-shell-safe-by-design-3ed6</link>
      <guid>https://dev.to/goweft/seven-agents-zero-trust-how-i-made-an-agentic-shell-safe-by-design-3ed6</guid>
      <description>&lt;p&gt;Earlier this year I wrote about &lt;a href="https://dev.to/goweft/i-replaced-my-ai-chat-interface-with-a-terminal-shell-5aoh"&gt;CAS&lt;/a&gt;, a terminal shell where conversation and direct manipulation live in the same window. That post was about the interface. This one is about the part underneath that I care about more: how the thing decides what it's allowed to do.&lt;/p&gt;

&lt;p&gt;The question that drove the whole design is simple to state and hard to answer well: &lt;strong&gt;when an LLM can call tools, browse the web, and act across multiple connected services, how do you keep it inside the lines?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The usual answer is to ask the model nicely in a system prompt and hope. I didn't want to hope.&lt;/p&gt;




&lt;h2&gt;
  
  
  The shape of the problem
&lt;/h2&gt;

&lt;p&gt;CAS grew a set of capabilities that each let the model reach further out into the world:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It connects to MCP servers and calls their tools.&lt;/li&gt;
&lt;li&gt;It fetches and reads web pages.&lt;/li&gt;
&lt;li&gt;It coordinates tasks that span two or more of those connected workspaces at once.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of those is a place where a confused or adversarial model could do something I didn't ask for. Call the wrong tool. Navigate somewhere it invented. Route a step to a workspace that wasn't part of the task. The more capable the system got, the more these seams mattered.&lt;/p&gt;

&lt;p&gt;I decided early that the safety story could not live in prompts. Prompts are suggestions. I wanted guarantees that hold regardless of what the model outputs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Every LLM call has a named owner
&lt;/h2&gt;

&lt;p&gt;The first move was structural. In CAS, the shell — the thing that routes your messages — makes &lt;strong&gt;zero&lt;/strong&gt; LLM calls. Not one. Every call to a model is owned by a named agent with a single responsibility:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GenerationAgent    creates new workspace content
EditAgent          applies a change to existing content
CombineAgent       merges multiple workspaces into one
ChatAgent          handles conversational turns
MCPAgent           plans and executes MCP tool calls
WebAgent           plans and executes web actions
OrchestratorAgent  coordinates multi-workspace tasks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven agents. The shell detects intent (with regex, no model involved — that part's in the &lt;a href="https://dev.to/goweft/i-replaced-my-ai-chat-interface-with-a-terminal-shell-5aoh"&gt;last post&lt;/a&gt;) and delegates to exactly one of them. The agent owns the call, the prompt, and crucially the &lt;strong&gt;contract&lt;/strong&gt; around the call.&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%2F0ggc14dnhg7x1akiwgsm.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%2F0ggc14dnhg7x1akiwgsm.png" alt="The shell routes each message to exactly one of seven named agents; every model call has a named owner." width="800" height="511"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This matters because it gives every model interaction a boundary with a name on it. When something goes wrong, the error says &lt;code&gt;mcp-agent: precondition violation: connection_has_tools&lt;/code&gt;. You always know which agent, which phase, which rule.&lt;/p&gt;




&lt;h2&gt;
  
  
  Contracts the model can't see
&lt;/h2&gt;

&lt;p&gt;The boundary itself is a contract, in the &lt;a href="https://en.wikipedia.org/wiki/Design_by_contract" rel="noopener noreferrer"&gt;Bertrand Meyer&lt;/a&gt; sense — preconditions checked before the work, postconditions checked after. The whole type is about thirty lines of Go:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Rule&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt;        &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Description&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Check&lt;/span&gt;       &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Contract&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;AgentName&lt;/span&gt;      &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Preconditions&lt;/span&gt;  &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;Rule&lt;/span&gt;
    &lt;span class="n"&gt;Postconditions&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;Rule&lt;/span&gt;
    &lt;span class="n"&gt;frozen&lt;/span&gt;         &lt;span class="kt"&gt;bool&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three properties make this work as a safety mechanism rather than decoration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It runs in Go, outside the model.&lt;/strong&gt; The checks are ordinary functions. The model never sees them, can't reason about them, and has no path to modify them. There is no prompt injection that reaches a &lt;code&gt;func() bool&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's frozen before the call.&lt;/strong&gt; An agent constructs its contract and calls &lt;code&gt;Freeze()&lt;/code&gt; before it ever talks to the model. The agent cannot loosen its own constraints partway through. The contract for a given operation is fixed before the operation begins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It fails closed.&lt;/strong&gt; A violation returns an error and stops the operation. No fallback, no retry, no "let me ask the model to fix it." If a postcondition fails, the work is discarded.&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%2Fh1j2j35dxivk6z8hqgwq.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%2Fh1j2j35dxivk6z8hqgwq.png" alt="The contract envelope: preconditions, the LLM call, then postconditions; any violation discards the operation, fail-closed." width="800" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's what that looks like in practice for the agent that creates content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Preconditions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;contract&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;"workspace_type_allowed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Check&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;bool&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;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WSType&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"document"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
                   &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WSType&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"code"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
                   &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WSType&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"list"&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;"prompt_not_empty"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Check&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;bool&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;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Postconditions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;contract&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;"content_not_empty"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Check&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;bool&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;contentSize&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;"content_size_within_limit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Check&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;bool&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;contentSize&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;maxContentBytes&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing surprising. That's the point. The constraints are boring, legible, and enforced by the compiler-checked language rather than the probabilistic one.&lt;/p&gt;




&lt;h2&gt;
  
  
  The interesting postconditions
&lt;/h2&gt;

&lt;p&gt;The boring checks earn their keep. But a few postconditions are doing real work that a prompt could never guarantee.&lt;/p&gt;

&lt;p&gt;The edit agent has a &lt;strong&gt;truncation guard&lt;/strong&gt;. A common failure mode when you ask a model to "add a section" to a long document is that it returns just the new section — or a summarized version of the whole thing — instead of the full updated content. Silent data loss. The postcondition catches it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"result_not_drastically_shorter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Check&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;bool&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;contentSize&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;minExpected&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c"&gt;// minExpected is 10% of the original length&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the model returns something less than a tenth the size of what it was editing, the operation fails closed and the original content is untouched. The model can't lose your document by misunderstanding the task.&lt;/p&gt;

&lt;p&gt;The MCP agent has a &lt;strong&gt;tool-existence check&lt;/strong&gt;. When the model decides which tool to call, the postcondition verifies the chosen tool name actually exists on the connected server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"tool_name_known"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Check&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;bool&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;toolCall&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Connection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tools&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;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;toolCall&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToolName&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A model that hallucinates a tool called &lt;code&gt;delete_everything&lt;/code&gt; never gets to call it, because &lt;code&gt;delete_everything&lt;/code&gt; isn't in the server's advertised tool list and the contract rejects the plan before execution.&lt;/p&gt;

&lt;p&gt;The web agent does the same thing for URLs — a navigation target has to be a real, parseable, absolute URL, not something the model invented mid-sentence.&lt;/p&gt;




&lt;h2&gt;
  
  
  Coordinating agents without trusting them
&lt;/h2&gt;

&lt;p&gt;The hardest case is the orchestrator. When you say &lt;em&gt;"read the issue in the Linear workspace and open a GitHub PR for it,"&lt;/em&gt; one instruction spans two connected services. The orchestrator's job is to decompose that into ordered steps and run them.&lt;/p&gt;

&lt;p&gt;This is exactly where an agentic system can go off the rails — a multi-step plan is a lot of surface area. So the orchestrator never touches a workspace directly. It produces a plan, and a separate interface executes each step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;StepExecutor&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ExecuteStep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wsID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;instruction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prior&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The orchestrator knows nothing about MCP servers or web sessions. It emits steps; the shell routes each to the right agent. And the orchestrator's contract enforces the one rule that matters: &lt;strong&gt;every step in the plan must target a workspace that actually exists.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"plan_steps_have_known_workspaces"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Check&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Steps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;idSet&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WorkspaceID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model is handed a list of real workspace IDs and asked to plan against them. If its plan references an ID that wasn't in the list, the plan is rejected before a single step runs. The model can't route work to a service it dreamed up.&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%2Fqayrj76skwxosxrxmhdz.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%2Fqayrj76skwxosxrxmhdz.png" alt="The orchestrator produces a validated plan and emits steps through StepExecutor; the shell routes each step to the right agent." width="800" height="492"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Output from each step is fed as context into the next, so the GitHub step sees what the Linear step found — but the &lt;em&gt;structure&lt;/em&gt; of the plan is validated by code, not trusted from the model.&lt;/p&gt;




&lt;h2&gt;
  
  
  A dial for how much rope
&lt;/h2&gt;

&lt;p&gt;Validation handles "is this action well-formed." It doesn't handle "do I actually want this action to happen." For that there's an autonomy dial, and it's per-operation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;suggest&lt;/strong&gt; — the agent plans the action and shows it to you. Nothing executes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;confirm&lt;/strong&gt; — the agent executes, but pauses before each step for your approval.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;run&lt;/strong&gt; — the agent executes freely within its workspace scope.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the terminal, confirm mode is a real pause. The agent's goroutine blocks on a channel while the UI shows you what's about to happen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  CONFIRM  │  [ws-linear] list open issues  │  y: proceed  │  n: skip  │  esc: cancel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Press &lt;code&gt;y&lt;/code&gt; and the step runs. Press &lt;code&gt;n&lt;/code&gt; and it's skipped. Press &lt;code&gt;esc&lt;/code&gt; and you bail. The goroutine is genuinely waiting — no polling, no timeout games, just a blocking read on a channel that the keypress writes to. Human-in-the-loop, implemented as backpressure.&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%2F85jya6p92lsfnzy4oizx.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%2F85jya6p92lsfnzy4oizx.png" alt="The autonomy dial: suggest plans only, confirm pauses before each step, run executes freely within scope." width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And because every step — what it was, what it returned — is written to SQLite as it happens, there's a full audit trail afterward. You can reconstruct exactly what the system did and what flowed between steps.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this buys
&lt;/h2&gt;

&lt;p&gt;None of these pieces is clever on its own. A contract is an if-statement. A tool-existence check is a loop. The autonomy dial is a blocking channel read.&lt;/p&gt;

&lt;p&gt;The value is in where they sit. The model proposes; Go disposes. Every reach into the outside world passes through a named boundary with constraints that were fixed before the model spoke, that the model can't see or alter, and that fail closed when violated. The security-relevant decisions live in the deterministic layer, not the probabilistic one.&lt;/p&gt;

&lt;p&gt;It turns out a 40-year-old idea about software correctness — preconditions, postconditions, fail closed — is a remarkably good fit for the problem of keeping a language model inside the lines. Meyer was writing about catching programmer mistakes. The same machinery catches model mistakes just as well, and for the same reason: it doesn't trust the caller to be correct.&lt;/p&gt;

&lt;p&gt;CAS is open source — &lt;a href="https://github.com/goweft/cas" rel="noopener noreferrer"&gt;github.com/goweft/cas&lt;/a&gt;. The contract layer is &lt;code&gt;internal/contract&lt;/code&gt;, and each agent's contract lives next to it in &lt;code&gt;internal/agent&lt;/code&gt;. It's all about as boring as the snippets above, which is exactly what I wanted from a safety layer.&lt;/p&gt;

</description>
      <category>go</category>
      <category>ai</category>
      <category>security</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Replaced My AI Chat Interface With a Terminal Shell</title>
      <dc:creator>Steve Gonzalez</dc:creator>
      <pubDate>Mon, 06 Apr 2026 05:16:21 +0000</pubDate>
      <link>https://dev.to/goweft/i-replaced-my-ai-chat-interface-with-a-terminal-shell-5aoh</link>
      <guid>https://dev.to/goweft/i-replaced-my-ai-chat-interface-with-a-terminal-shell-5aoh</guid>
      <description>&lt;p&gt;Most AI tools give you a chat window. You type, the model responds, you copy what you need and paste it somewhere else. The conversation and the artifact live in different places.&lt;/p&gt;

&lt;p&gt;I wanted the artifact to appear &lt;em&gt;next to the conversation&lt;/em&gt;, stream in as it was generated, and stay there — editable, persistent, tab-switchable — without ever leaving the terminal.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;CAS&lt;/strong&gt;: Conversational Agent Shell.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─ chat ──────────────────────┐ ┌─ [l] Todo List For Easter ──────────────┐
│                             │ │                                          │
│ you › make a todo list for  │ │  Easter Todo List                        │
│       easter                │ │                                          │
│                             │ │  ## 🗓 Planning &amp;amp; Budget                 │
│ cas › Created list          │ │                                          │
│       workspace "Todo List  │ │  [ ] Set date and time for Easter Sunday │
│       For Easter".          │ │  [ ] Confirm guest list and RSVPs        │
│       Edit directly or ask  │ │  [ ] Create budget for food              │
│       me to make changes.   │ │  [ ] Check family availability           │
│                             │ │  [ ] Book reservations                   │
│ &amp;gt; █                         │ │                                          │
└─────────────────────────────┘ └──────────────────────────────────────────┘
  ↑↓ scroll  │  enter send  │  tab workspace  │  ctrl+n new session
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Left panel: conversation. Right panel: the workspace, streaming in as the model generates it. You stay in the terminal the whole time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;There's a debate in HCI that goes back to 1997.&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.amazonaws.com%2Fuploads%2Farticles%2F1q0qa4fa0vq74u72xfnf.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.amazonaws.com%2Fuploads%2Farticles%2F1q0qa4fa0vq74u72xfnf.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ben Shneiderman argued that direct manipulation gives users control that delegation never can. Pattie Maes argued that agents reduce cognitive load that direct manipulation can't scale to. Both were right. They were arguing about the wrong dichotomy.&lt;/p&gt;

&lt;p&gt;CAS resolves it architecturally: &lt;strong&gt;agents generate, users manipulate.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You describe what you want. The agent produces it. Once it exists, you own it — you edit it directly, you scroll it, you tab between workspaces, you undo changes. The agent is a producer. You are the controller.&lt;/p&gt;

&lt;h2&gt;
  
  
  How messages flow
&lt;/h2&gt;

&lt;p&gt;Every message passes through a zero-latency routing layer before any model is called.&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.amazonaws.com%2Fuploads%2Farticles%2F7jnj6ibwovxkuexmyxop.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.amazonaws.com%2Fuploads%2Farticles%2F7jnj6ibwovxkuexmyxop.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Intent detection is pure regex — sub-millisecond, deterministic. The routing decision fires before the LLM even knows a message arrived.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"write a project proposal"          → create workspace (document)
"make a todo list"                   → create workspace (list)
"create a python script"            → create workspace (code)
"add a conclusion section"          → edit active workspace
"run it"                            → execute code workspace
"combine the proposal and checklist" → merge workspaces
"standup"                           → run Lua plugin
"how long should this be?"          → chat reply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plugins are checked first. Then close, run, combine, edit, create — in that priority order. Self-edit phrases like "I'll fix it myself" are caught before the edit patterns fire. The ordering matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deterministic contracts
&lt;/h2&gt;

&lt;p&gt;Every workspace operation passes through a contract layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;contract&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CheckPreconditions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c"&gt;// is this operation permitted?&lt;/span&gt;
&lt;span class="n"&gt;contract&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CheckInvariants&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;      &lt;span class="c"&gt;// are all invariants satisfied?&lt;/span&gt;
&lt;span class="n"&gt;contract&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CheckPostconditions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c"&gt;// did the output meet requirements?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These run in Go, not in the model. The model cannot modify, bypass, or reason about them. Any violation fails the operation closed. Based on Bertrand Meyer's Design by Contract (1986) — a 40-year-old idea that turns out to be exactly right for agentic systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code execution
&lt;/h2&gt;

&lt;p&gt;Say &lt;code&gt;run it&lt;/code&gt; with an active code workspace. CAS detects the language from content (bash, Python, Go, JavaScript, Ruby), writes to a temp file, and executes in a sandboxed subprocess.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;you › create a python script to compute fibonacci
     → [c] tab opens, tokens stream in

you › run it
     → ran python (23ms, exit 0)
       1, 1, 2, 3, 5, 8, 13, 21, 34, 55
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Process group isolation, restricted environment (only &lt;code&gt;PATH&lt;/code&gt; inherited), 30-second timeout that kills the entire tree. No LLM call — intent detection routes directly to the runner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-workspace operations
&lt;/h2&gt;

&lt;p&gt;With multiple tabs open, CAS resolves which workspace you're addressing by fuzzy-matching title fragments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"update the proposal"                → targets "Project Proposal"
"add the script code to the report"  → edits Report with Script as LLM context
"combine the proposal and checklist" → new workspace from both sources
"merge all workspaces"               → synthesizes everything into one
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Edits that reference another workspace by name include that workspace's content in the LLM prompt automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lua plugins
&lt;/h2&gt;

&lt;p&gt;Drop &lt;code&gt;.lua&lt;/code&gt; files in &lt;code&gt;~/.cas/plugins/&lt;/code&gt; to add custom commands without recompiling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ~/.cas/plugins/standup.lua&lt;/span&gt;
&lt;span class="n"&gt;cas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"standup"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Daily standup"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;workspaces&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;ipairs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"- "&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="s2"&gt;" ("&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="s2"&gt;")"&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="n"&gt;cas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;table.concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Type &lt;code&gt;standup&lt;/code&gt; and the plugin runs — no LLM call, sub-millisecond. The Lua VM is sandboxed: no file I/O, no &lt;code&gt;os.execute&lt;/code&gt;, no network. API: &lt;code&gt;cas.command()&lt;/code&gt;, &lt;code&gt;cas.reply()&lt;/code&gt;, &lt;code&gt;cas.workspaces()&lt;/code&gt;, &lt;code&gt;cas.active()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-provider
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Ollama — local, private, no API key&lt;/span&gt;
./cas

&lt;span class="c"&gt;# Anthropic — cloud, no GPU required&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CAS_PROVIDER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;anthropic
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-ant-...
./cas
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Documents and lists route to &lt;code&gt;qwen3.5:9b&lt;/code&gt; locally or Sonnet on Anthropic. Code routes to &lt;code&gt;qwen2.5-coder:7b&lt;/code&gt; locally or Haiku. All overridable via env vars.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;Single static Go binary. No runtime, no server, no browser. SSH to a remote machine and run it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;internal/
├── intent/      Regex intent detection — 7 intent kinds
├── contract/    Design by Contract enforcement
├── workspace/   Lifecycle: create, update, undo, close
├── shell/       Session manager + workspace resolver
├── llm/         Ollama + Anthropic streaming
├── runner/      Code execution — sandboxed subprocess
├── plugin/      Lua plugin runtime (gopher-lua)
├── store/       SQLite (WAL) + in-memory store
└── conductor/   Behavioral learning
ui/              Bubble Tea TUI: split panel, tabs, streaming
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;245 tests across all packages. 8 TUI integration tests that spawn the real binary in tmux and interact with it as a user would.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Requires Go 1.25+&lt;/span&gt;
git clone https://github.com/goweft/cas.git
&lt;span class="nb"&gt;cd &lt;/span&gt;cas
go build &lt;span class="nt"&gt;-o&lt;/span&gt; cas ./cmd/cas

&lt;span class="c"&gt;# Local inference&lt;/span&gt;
ollama pull qwen3.5:9b &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ollama pull qwen2.5-coder:7b
./cas

&lt;span class="c"&gt;# Or cloud&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CAS_PROVIDER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;anthropic
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-key
./cas
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why a terminal
&lt;/h2&gt;

&lt;p&gt;It's already where the work happens. It composes with existing tools — export to markdown, pipe to pandoc, commit to git. And it works over SSH: run CAS on a machine with a GPU, access it from a laptop without one.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/goweft/cas" rel="noopener noreferrer"&gt;goweft/cas&lt;/a&gt; — Apache 2.0&lt;/p&gt;

</description>
      <category>go</category>
      <category>ai</category>
      <category>terminal</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Found Anthropic's Source Map in a Production Bundle - So I Built Five Security Tools published.</title>
      <dc:creator>Steve Gonzalez</dc:creator>
      <pubDate>Mon, 06 Apr 2026 05:04:16 +0000</pubDate>
      <link>https://dev.to/goweft/i-found-anthropics-source-map-in-a-production-bundle-so-i-built-five-security-tools-published-215f</link>
      <guid>https://dev.to/goweft/i-found-anthropics-source-map-in-a-production-bundle-so-i-built-five-security-tools-published-215f</guid>
      <description>&lt;p&gt;On March 31, 2026, I was reviewing a Claude Code release when I found something unexpected: a complete JavaScript source map — a &lt;code&gt;.js.map&lt;/code&gt; file — shipped inside the production bundle. Source maps are development artifacts. They contain the original, pre-minified source code, internal file paths, variable names, and architectural structure. In a production bundle, they're a blueprint of your codebase handed to anyone who looks.&lt;/p&gt;

&lt;p&gt;This wasn't an Anthropic-specific failure. Source map leakage is one of the most common pre-publish mistakes in modern JavaScript tooling. Bundlers generate them by default. Developers forget to exclude them. CI pipelines don't check for them. And AI coding tools — which generate and publish code faster than any human can review — make the problem worse.&lt;/p&gt;

&lt;p&gt;I built five open-source security tools in response. This post explains what I found, why it matters for AI agent systems specifically, and what each tool does.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Source Map Leak Actually Exposes
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;.js.map&lt;/code&gt; file contains the original unminified source code, internal file paths and project structure, pre-mangled variable and function names, and source-to-output mappings that let anyone reconstruct your build process.&lt;/p&gt;

&lt;p&gt;For a company like Anthropic, this means internal architecture details, module boundaries, and naming conventions that would normally take months of reverse engineering — handed over in a single file.&lt;/p&gt;

&lt;p&gt;For any organization shipping AI agents, the risk is compounded: agents generate and publish code autonomously, often faster than security review can keep up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AI Tooling Makes This Worse
&lt;/h2&gt;

&lt;p&gt;Traditional developer tools have a human in the loop at publish time. You run &lt;code&gt;npm publish&lt;/code&gt;, you notice the 847KB &lt;code&gt;.map&lt;/code&gt; file in the tarball, you stop.&lt;/p&gt;

&lt;p&gt;AI coding agents change this. An agent that can write, commit, and publish code can do all three faster than a human can review. The attack surface isn't just "developer forgets to exclude source maps" — it's "agent generates a release, publishes it, and the source map was never on anyone's checklist."&lt;/p&gt;

&lt;p&gt;This is the gap the five tools address. Not fixing the underlying problem (that's a toolchain problem), but making the gap visible and catchable before it becomes public.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. tenter — Pre-publish artifact scanner
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Repos:&lt;/strong&gt; &lt;a href="https://github.com/goweft/tenter" rel="noopener noreferrer"&gt;goweft/tenter&lt;/a&gt; (Python, GitHub Actions) · &lt;a href="https://github.com/goweft/tenter-rs" rel="noopener noreferrer"&gt;goweft/tenter-rs&lt;/a&gt; (Rust, static binary)&lt;/p&gt;

&lt;p&gt;tenter scans a directory before publish and fails if it finds artifacts that shouldn't ship: source maps, &lt;code&gt;.env&lt;/code&gt; files, private keys, debug builds, or secrets matching common patterns.&lt;/p&gt;

&lt;p&gt;v1 ships as a GitHub Action on the Marketplace — three lines of YAML, zero config:&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="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;goweft/tenter@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./dist&lt;/span&gt;
    &lt;span class="na"&gt;fail-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;source-maps,env-files,secrets&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;v2 (&lt;code&gt;tenter-rs&lt;/code&gt;) is a Rust rewrite: a single ~2MB static binary, no runtime dependencies, identical rule set and config format. Runs anywhere including minimal containers and non-GitHub CI.&lt;/p&gt;

&lt;p&gt;The source map that triggered this whole sprint would have failed a tenter scan immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. unshear — Fork divergence detector
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/goweft/unshear" rel="noopener noreferrer"&gt;goweft/unshear&lt;/a&gt; · Rust&lt;/p&gt;

&lt;p&gt;When someone forks an AI agent framework and removes safety mechanisms, unshear finds the delta. It compares a forked codebase against its upstream and surfaces files where safety-related patterns — guardrails, validation, rate limits, audit logging — were removed or weakened.&lt;/p&gt;

&lt;p&gt;Named after the shear lines in composite materials: the place where layers separate under stress. A forked agent that stripped its safety layer looks structurally similar to the original until you pull on it.&lt;/p&gt;

&lt;p&gt;Rust was a deliberate choice: a security tool that itself has 200 transitive dependencies is a liability.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. ratine — Agent memory poisoning detection
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/goweft/ratine" rel="noopener noreferrer"&gt;goweft/ratine&lt;/a&gt; · Python&lt;/p&gt;

&lt;p&gt;Ratine detects prompt injection attempts in agent memory stores. As agents accumulate context — conversation history, retrieved documents, tool results — that context becomes an attack surface. A malicious document retrieved during a research task can contain instructions that persist into future agent actions.&lt;/p&gt;

&lt;p&gt;Ratine scans memory stores (ChromaDB, plain JSON, SQLite) for patterns consistent with injection: instruction-like language in unexpected positions, escalation patterns, attempts to override system-level constraints.&lt;/p&gt;

&lt;p&gt;Named after a type of textured yarn — the attack surface is threaded through otherwise normal content.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. crocking — AI authorship detection
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/goweft/crocking" rel="noopener noreferrer"&gt;goweft/crocking&lt;/a&gt; · Python&lt;/p&gt;

&lt;p&gt;Crocking identifies code likely generated by an LLM. This matters for supply chain security: AI-generated code has characteristic patterns that differ from human-written code, and knowing provenance helps assess risk. Code that was generated, not written, may not have been reviewed with the same scrutiny.&lt;/p&gt;

&lt;p&gt;This is not about whether AI-generated code is "bad." It's about provenance transparency — knowing what you're actually running.&lt;/p&gt;

&lt;p&gt;Named after the textile term for dye that rubs off. The AI fingerprint is often visible if you know what to look for.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. heddle — Runtime trust enforcement
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/goweft/heddle" rel="noopener noreferrer"&gt;goweft/heddle&lt;/a&gt; · Python&lt;/p&gt;

&lt;p&gt;Heddle is the most architectural of the five. It's a self-hosted MCP (Model Context Protocol) mesh runtime where agents are defined as YAML configs, auto-register as MCP servers, and can bidirectionally consume and expose tools.&lt;/p&gt;

&lt;p&gt;Every tool call passes through deterministic contract enforcement before execution. Trust tiers (T1 read-only through T4 admin) control what each agent can do. Every action is audit-logged. The security model maps directly to OWASP Agentic Top 10 and NIST AI RMF.&lt;/p&gt;

&lt;p&gt;The name comes from the heddle in a loom — the component that controls which threads are lifted. Security is in the architecture, not bolted on afterward.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Source Map Leak Actually Tells Us
&lt;/h2&gt;

&lt;p&gt;The Anthropic source map incident was minor in isolation. No credentials were exposed, no production systems were affected. But it's a useful signal: even organizations with mature security practices miss pre-publish checks on non-traditional artifact types.&lt;/p&gt;

&lt;p&gt;AI tooling generates a category of artifact — bundles, packages, compiled agents, memory exports — that existing security tooling wasn't designed to inspect. The gap isn't in the tools that exist; it's in the tools that don't exist yet.&lt;/p&gt;

&lt;p&gt;These five tools are a start. They're all open source, every repo has tests and CI. The more interesting question is what the full picture looks like when AI agents are generating and publishing code at scale, autonomously, faster than human review can keep up.&lt;/p&gt;

&lt;p&gt;That's the problem worth solving.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Links&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/goweft/tenter" rel="noopener noreferrer"&gt;goweft/tenter&lt;/a&gt; — Python, GitHub Marketplace&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/goweft/tenter-rs" rel="noopener noreferrer"&gt;goweft/tenter-rs&lt;/a&gt; — Rust static binary&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/goweft/unshear" rel="noopener noreferrer"&gt;goweft/unshear&lt;/a&gt; — Rust&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/goweft/ratine" rel="noopener noreferrer"&gt;goweft/ratine&lt;/a&gt; — Python&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/goweft/crocking" rel="noopener noreferrer"&gt;goweft/crocking&lt;/a&gt; — Python&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/goweft/heddle" rel="noopener noreferrer"&gt;goweft/heddle&lt;/a&gt; — Python, MCP runtime&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>opensource</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Security Gap in MCP Tool Servers (And What I Built to Fix It)</title>
      <dc:creator>Steve Gonzalez</dc:creator>
      <pubDate>Wed, 25 Mar 2026 22:36:59 +0000</pubDate>
      <link>https://dev.to/goweft/the-security-gap-in-mcp-tool-servers-and-what-i-built-to-fix-it-1hlg</link>
      <guid>https://dev.to/goweft/the-security-gap-in-mcp-tool-servers-and-what-i-built-to-fix-it-1hlg</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; there's an updated and expanded version of this post — it covers the independent security review Heddle went through, the four findings it produced, and what shipped to close them. &lt;a href="https://dev.to/goweft/the-security-gap-in-mcp-tool-servers-and-what-i-built-to-fix-it-5c"&gt;Read the current version instead&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;MCP has no security model. I built Heddle — a policy-and-trust layer that turns YAML configs into validated, policy-enforced MCP tool servers.&lt;/p&gt;

&lt;p&gt;MCP (Model Context Protocol) is how AI agents connect to tools. Claude Desktop uses it, Cursor uses it, and thousands of developers are building MCP servers to give AI access to their APIs, databases, and infrastructure.&lt;/p&gt;

&lt;p&gt;There's one problem: &lt;strong&gt;MCP has no security model.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The protocol defines how a client talks to a server, but says nothing about what that server is allowed to do. No authentication between client and server. No authorization on which tools can be called. No audit trail of what happened. The spec assumes you'll handle all of that yourself.&lt;/p&gt;

&lt;p&gt;Most people don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Goes Wrong
&lt;/h2&gt;

&lt;p&gt;I run a self-hosted server with Prometheus, Grafana, Ollama, Gitea, and a handful of other services. I wanted Claude Desktop to query all of them through MCP. The standard approach is to write a Python FastMCP server for each one — a few dozen lines per service, hardcode the API key, register the tools, done.&lt;/p&gt;

&lt;p&gt;That works until you think about what you've actually built:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every MCP server has full access to whatever its process can reach.&lt;/strong&gt; Your Prometheus tool can also hit your Grafana API, your Gitea API, and anything else on localhost. There's no scoping.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API keys live in environment variables or config files.&lt;/strong&gt; If you have 9 MCP servers, you have 9 places where credentials sit in plaintext with no access policy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nothing is logged.&lt;/strong&gt; If Claude calls a tool that restarts a service or deletes data, there's no record of which tool was called, with what parameters, by which agent, at what time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There's no concept of read-only vs. write.&lt;/strong&gt; A tool either exists or it doesn't. MCP doesn't know that &lt;code&gt;query_prometheus&lt;/code&gt; is safe to call freely but &lt;code&gt;restart_service&lt;/code&gt; should require approval.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool composition creates emergent risks.&lt;/strong&gt; When Claude has access to multiple MCP servers, it can chain calls across them. Server A reads sensitive data, Server B posts to an external API — Claude could combine them in ways neither server was designed for.&lt;/p&gt;

&lt;p&gt;These aren't theoretical risks. During development, I declared an agent as read-only (Trust Tier 1) but gave it a tool that used HTTP POST. The system I built caught it — blocked the call, logged a trust violation, and forced me to either fix the config or explicitly upgrade the trust level. Without that enforcement, the tool would have silently worked and I'd never have known my security model was wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/goweft/heddle" rel="noopener noreferrer"&gt;Heddle&lt;/a&gt; is a runtime that sits between your YAML config and the MCP protocol. You define your tools in a config file, and Heddle validates, secures, and serves them — with policy enforcement on every call.&lt;/p&gt;

&lt;p&gt;Here's a complete tool server for Prometheus:&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;agent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prometheus-bridge&lt;/span&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.0.0"&lt;/span&gt;
  &lt;span class="na"&gt;exposes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;query_prometheus&lt;/span&gt;
      &lt;span class="na"&gt;access&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Run&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;PromQL&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;query"&lt;/span&gt;
      &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;string&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;required&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;get_alerts&lt;/span&gt;
      &lt;span class="na"&gt;access&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;List&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;active&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Prometheus&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;alerts"&lt;/span&gt;
  &lt;span class="na"&gt;http_bridge&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_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;query_prometheus&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GET&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:9090/api/v1/query"&lt;/span&gt;
      &lt;span class="na"&gt;query_params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;query&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_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;get_alerts&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GET&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:9090/api/v1/alerts"&lt;/span&gt;
  &lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;trust_tier&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;heddle run agents/prometheus-bridge.yaml&lt;/code&gt; and Claude can query Prometheus in natural language. But every call goes through a six-layer dispatch pipeline before it reaches the API:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate limiting&lt;/strong&gt; → &lt;strong&gt;Access mode check&lt;/strong&gt; → &lt;strong&gt;Escalation rules&lt;/strong&gt; → &lt;strong&gt;Input validation&lt;/strong&gt; → &lt;strong&gt;Trust tier enforcement&lt;/strong&gt; → &lt;strong&gt;HTTP bridge execution&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Each layer can independently block the call and log why.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Security Controls
&lt;/h2&gt;

&lt;p&gt;The dispatch pipeline enforces these controls on every tool call:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trust Tiers (T1–T4).&lt;/strong&gt; Each config declares a trust level. T1 (observer) can only use GET — any POST/PUT/DELETE is blocked at runtime, not just warned. T2 (worker) allows scoped writes. T3 (operator) allows cross-agent invocation. T4 (privileged) requires human approval. I caught a real misconfiguration with this — a T1 agent tried to POST and the enforcer blocked it before the request ever left the process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Access Mode Annotations.&lt;/strong&gt; Every tool is declared as &lt;code&gt;access: read&lt;/code&gt; or &lt;code&gt;access: write&lt;/code&gt;. T1 configs with write tools are rejected at load time — before the server even starts. This is the schema-level version of least privilege.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Credential Broker.&lt;/strong&gt; API keys are stored in &lt;code&gt;~/.heddle/secrets.json&lt;/code&gt; with per-config access policies. Configs reference them as &lt;code&gt;{{secret:prometheus-token}}&lt;/code&gt; — resolved at runtime, never written to the YAML file. A config can only access secrets it's been explicitly granted. Unauthorized access is denied, logged, and returns a placeholder instead of the real value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Escalation Rules.&lt;/strong&gt; Declarative conditions that hold a tool call for review instead of executing it. For example, my VRAM orchestrator has a rule that holds any &lt;code&gt;smart_load&lt;/code&gt; call if the model name contains "27b" — because loading a 27-billion parameter model consumes most of my 24GB GPU memory. The rule triggers, the call is held, and the audit log records why.&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;escalation_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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;large-model-load&lt;/span&gt;
    &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Loading&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&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;that&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;will&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;consume&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;most&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&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;24GB&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;VRAM"&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;smart_load"&lt;/span&gt;
    &lt;span class="na"&gt;param_contains&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;model_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;27b"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Input Validation.&lt;/strong&gt; Type checking, length limits, and injection pattern detection on every parameter. The validator catches shell injection (&lt;code&gt;; rm -rf /&lt;/code&gt;), SQL injection (&lt;code&gt;' OR 1=1&lt;/code&gt;), path traversal (&lt;code&gt;../../etc/passwd&lt;/code&gt;), and LLM prompt injection (&lt;code&gt;ignore previous instructions&lt;/code&gt;). In strict mode, these are blocked. In permissive mode, they're logged and passed through.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hash-Chained Audit Log.&lt;/strong&gt; Every tool call, trust violation, credential access, and escalation hold is logged as a JSON Lines entry. Each entry includes a SHA-256 hash of the previous entry — if anyone modifies or deletes a log entry, the chain breaks and verification fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Config Signing.&lt;/strong&gt; All YAML configs are signed with HMAC-SHA256. If a config is modified after signing, the runtime detects the tampering. AI-generated configs (from Heddle's natural language generator) are automatically quarantined in a staging directory until explicitly promoted.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Looks Like Running
&lt;/h2&gt;

&lt;p&gt;I'm currently running 46 tools from 9 configs through a single MCP connection to Claude Desktop. The configs cover Prometheus, Grafana, Ollama, Gitea, an RSS aggregator, a RAG search API, a GPU VRAM orchestrator, and a daily operations briefing agent.&lt;/p&gt;

&lt;p&gt;Every one of those 46 tools goes through the same dispatch pipeline. The Prometheus tools are T1 (read-only, 5 tools). The Ollama bridge is T2 (can POST for text generation). The VRAM orchestrator is T3 (can invoke other agents, has escalation rules on destructive operations).&lt;/p&gt;

&lt;p&gt;The trust tiers aren't just labels — they're enforced. A T1 config physically cannot make a POST request, even if the HTTP bridge URL is correct and the API would accept it. The enforcer blocks it before the request is constructed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Framework Mapping
&lt;/h2&gt;

&lt;p&gt;Every security control maps to at least one industry framework. This matters if you're in an organization that needs to demonstrate compliance, or if you're building a portfolio that shows applied security architecture (which is why I built this):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;th&gt;OWASP Agentic Top 10&lt;/th&gt;
&lt;th&gt;NIST AI RMF&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Trust tiers&lt;/td&gt;
&lt;td&gt;#3 Excessive Agency&lt;/td&gt;
&lt;td&gt;GV-1.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Credential broker&lt;/td&gt;
&lt;td&gt;#7 Unsafe Credential Mgmt&lt;/td&gt;
&lt;td&gt;MAP-3.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit logging&lt;/td&gt;
&lt;td&gt;#9 Insufficient Logging&lt;/td&gt;
&lt;td&gt;MS-2.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input validation&lt;/td&gt;
&lt;td&gt;#1 Prompt Injection&lt;/td&gt;
&lt;td&gt;MS-2.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Config signing&lt;/td&gt;
&lt;td&gt;#8 Supply Chain&lt;/td&gt;
&lt;td&gt;GV-6.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Escalation rules&lt;/td&gt;
&lt;td&gt;#3 Excessive Agency&lt;/td&gt;
&lt;td&gt;GV-1.3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The full threat model with 8 threat categories is in the repo at &lt;a href="https://github.com/goweft/heddle/blob/master/docs/threat-model.md" rel="noopener noreferrer"&gt;docs/threat-model.md&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/goweft/heddle.git
&lt;span class="nb"&gt;cd &lt;/span&gt;heddle
python &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;".[dev]"&lt;/span&gt;

&lt;span class="c"&gt;# Try a starter pack&lt;/span&gt;
&lt;span class="nb"&gt;cp &lt;/span&gt;packs/prometheus.yaml agents/
heddle validate agents/prometheus.yaml
heddle run agents/prometheus.yaml &lt;span class="nt"&gt;--port&lt;/span&gt; 8200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Heddle ships with 6 starter packs — Prometheus, Grafana, Gitea/GitHub, Ollama, Sonarr, and Radarr — that you can drop into &lt;code&gt;agents/&lt;/code&gt; and run immediately. All read-only (T1) except Ollama (T2 for text generation).&lt;/p&gt;

&lt;p&gt;Or generate a config from natural language:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;heddle generate &lt;span class="s2"&gt;"agent that wraps the Home Assistant API"&lt;/span&gt; &lt;span class="nt"&gt;--model&lt;/span&gt; qwen3:14b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Works with Claude Desktop, Cursor, and any MCP client that supports stdio transport.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Heddle&lt;/strong&gt; is open source (MIT) at &lt;a href="https://github.com/goweft/heddle" rel="noopener noreferrer"&gt;github.com/goweft/heddle&lt;/a&gt;. 126 tests, 15 security controls, and a threat model mapped to OWASP Agentic Top 10 and NIST AI RMF. If you're exposing APIs to AI agents, I'd like to know what security controls you wish existed.&lt;/p&gt;

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