<?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: Rojaneer</title>
    <description>The latest articles on DEV Community by Rojaneer (@rojaneerdev).</description>
    <link>https://dev.to/rojaneerdev</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%2F4145316%2F4b26832a-0172-4456-bef6-c8c07a5461bc.png</url>
      <title>DEV Community: Rojaneer</title>
      <link>https://dev.to/rojaneerdev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rojaneerdev"/>
    <language>en</language>
    <item>
      <title>From god-mode to governed: an AI agent that can’t exceed its grant</title>
      <dc:creator>Rojaneer</dc:creator>
      <pubDate>Thu, 15 Oct 2026 00:13:35 +0000</pubDate>
      <link>https://dev.to/rojaneerdev/from-god-mode-to-governed-an-ai-agent-that-cant-exceed-its-grant-4ilk</link>
      <guid>https://dev.to/rojaneerdev/from-god-mode-to-governed-an-ai-agent-that-cant-exceed-its-grant-4ilk</guid>
      <description>&lt;p&gt;&lt;a href="https://dev.to/rojaneerdev/your-ai-agents-are-running-as-god-mode-service-accounts-iih"&gt;Last time&lt;/a&gt; I argued that AI agents are running as god-mode service accounts: an agent acts in your systems, and nobody can cleanly answer &lt;em&gt;who authorized that — which human, acting through which agent, with whose permissions, and was anything refused?&lt;/em&gt; I promised the next post would show the thing working.&lt;/p&gt;

&lt;p&gt;So here it is: a real agent, acting &lt;strong&gt;as me&lt;/strong&gt;, in a system that only knows how to talk to a human — unable to exceed what I granted it, and writing a receipt that answers all four questions. You can run the whole thing from the Apache repo. Bring a key or use the hermetic harness; read your own audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup: two programs, one rule
&lt;/h2&gt;

&lt;p&gt;There are two programs here, and keeping them separate is the whole point.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;agenthof&lt;/code&gt;&lt;/strong&gt; is the governance engine — &lt;code&gt;apply&lt;/code&gt;, &lt;code&gt;run&lt;/code&gt;, &lt;code&gt;audit&lt;/code&gt;, &lt;code&gt;investigate&lt;/code&gt;. It never embeds the agent or decides how it thinks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The agent&lt;/strong&gt; is a &lt;em&gt;separate&lt;/em&gt; program (a small LangChain agent in the repo). &lt;code&gt;agenthof&lt;/code&gt; calls it over a socket and drives it through &lt;strong&gt;doors&lt;/strong&gt;: model, tool, exec, spawn. Each door authorizes the call, injects the credential the agent never holds, and writes one ledger event bound to the human who started the run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I start the run &lt;strong&gt;as a verified human&lt;/strong&gt; — in the showcase a stub IdP issues Dana's token; in production your own IdP (Okta, Entra, Keycloak) mints it and &lt;code&gt;agenthof serve&lt;/code&gt; verifies it per call. Either way, from that point on, &lt;em&gt;Dana&lt;/em&gt; is on the wire, not a shared bot.&lt;/p&gt;

&lt;p&gt;The agent runs with &lt;strong&gt;no network and no secret of its own.&lt;/strong&gt; It can't call your CRM directly; it can only ask a door. When it asks to act on a per-user system, the gateway performs an &lt;strong&gt;RFC 8693 token exchange&lt;/strong&gt; — swapping Dana's verified token for one scoped to that upstream — and calls the system &lt;strong&gt;as Dana&lt;/strong&gt;. The secret never enters the agent's process.&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%2Ftin156bga4buceunoz7h.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%2Ftin156bga4buceunoz7h.png" alt="An agent acting as Dana and the receipt it leaves: Dana asks → the agent runs with no network and no secret → a door exchanges Dana's token (RFC 8693) and calls the SaaS as Dana → an over-scope call is refused → a hash-chained ledger records who asked (Dana), what ran (the agent), whose key acted (Dana, via on-behalf-of), and what was refused, with audit verify passing." width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the receipt
&lt;/h2&gt;

&lt;p&gt;After the run, &lt;code&gt;agenthof audit &amp;lt;run&amp;gt;&lt;/code&gt; prints the ledger for that run, in order. This is the receipt. Here it is from an actual hermetic run of &lt;code&gt;scripts/e2e-acceptance-llm-local.sh&lt;/code&gt; — I've elided the leading timestamps, a temp path, and the tails of run ids for width; nothing else is changed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ agenthof audit r-85f417c68560e228

run r-85f417c68560e228 — acceptance (role acceptance-operator)
invoked by dana@example.com (oidc, issuer http://127.0.0.1:50737)
status: succeeded
ledger integrity: verified (13 events)

  workflow started
  step drive (agent acceptance) started
  model fast — 3 prompt / 5 completion tokens
  exec env — exit 0 (runtime) [runtime-attested: refexec env pid 35795 spawn 1]
  model fast — 3 prompt / 5 completion tokens
  tool echo   — args 20effc4e (static_env) [runtime-attested: refbridge …/stdio-tool -credential-env DEMO_TOKEN pid 35759 spawn 1]
  tool whoami — args 44136fa3 (token_exchange)
  model fast — 3 prompt / 5 completion tokens
  spawn acceptance-worker/acceptance-sub → run r-e0fc07e8… succeeded (depth 1)
  spawn acceptance-worker/acceptance-sub → run r-09a303b8… succeeded (depth 1)
  model fast — 3 prompt / 5 completion tokens
  workflow finished: succeeded
config sha256:299d78f0… — applied by ci (asserted) at 2026-10-14T15:05:02Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Map it to the four questions from part 1:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Which human?&lt;/strong&gt; &lt;code&gt;invoked by dana@example.com (oidc, issuer …)&lt;/code&gt; — stamped on the run, and every event belongs to it. Not &lt;code&gt;svc-agent-bot&lt;/code&gt;. (In this hermetic run a stub issues Dana's token, so the issuer is a localhost address; in production it's your IdP.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Which agent, and what did it actually do?&lt;/strong&gt; The &lt;code&gt;model fast&lt;/code&gt;, &lt;code&gt;exec&lt;/code&gt;, &lt;code&gt;tool&lt;/code&gt;, and &lt;code&gt;spawn&lt;/code&gt; lines — each written &lt;strong&gt;first-hand&lt;/strong&gt; by Agenthof or the operator's runtime (&lt;code&gt;[runtime-attested: refexec …]&lt;/code&gt;, &lt;code&gt;[runtime-attested: refbridge …]&lt;/code&gt;), not by the agent. Note what's &lt;em&gt;not&lt;/em&gt; there: no prompt text, no completion text, and the &lt;code&gt;model fast&lt;/code&gt; line doesn't even reveal which tool the model asked for. The model's choices show up only as the door events they became.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whose key acted?&lt;/strong&gt; &lt;code&gt;tool whoami — … (token_exchange)&lt;/code&gt;. The agent called a per-user tool, but the gateway exchanged Dana's token (RFC 8693) and called it &lt;strong&gt;as Dana&lt;/strong&gt; — the step's own summary comes back &lt;code&gt;tool whoami: acting as: u-dana&lt;/code&gt;. The agent held only its run token; it never saw a secret.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Was anything refused?&lt;/strong&gt; Not in &lt;em&gt;this&lt;/em&gt; run — the model only reached for things its grant allowed. So point the same workflow at a role whose exec allowlist doesn't include that command, and watch the agent reach past its grant:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;agenthof audit r-c5cc1961…        &lt;span class="c"&gt;# same workflow, stricter allowlist&lt;/span&gt;

  …
  &lt;span class="nb"&gt;exec env &lt;/span&gt;refused — &lt;span class="nb"&gt;command &lt;/span&gt;is not on the &lt;span class="nb"&gt;exec &lt;/span&gt;allowlist
  …
  workflow finished: succeeded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The door said no and &lt;strong&gt;recorded the no&lt;/strong&gt;. The agent was told &lt;code&gt;refused by policy&lt;/code&gt;, the model moved on, and the step still &lt;em&gt;finished&lt;/em&gt; — a refusal in the ledger is governance working, not a crash. (With a real model driving, that reach can happen mid-run and you'd see the refusal inline; here it's forced deterministically.)&lt;/p&gt;

&lt;p&gt;And above all of it: &lt;code&gt;ledger integrity: verified&lt;/code&gt;, and &lt;code&gt;audit verify&lt;/code&gt; on each run returns the chain head:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ agenthof audit verify r-85f417c6…
head: 4d655340… (13 events)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ledger is append-only and hash-chained, so it catches an edit, deletion, or reordering that doesn't recompute every later hash. &lt;code&gt;agenthof investigate --run &amp;lt;run&amp;gt;&lt;/code&gt; prints the same story as one timeline across the whole tree — the parent plus each sub-agent's events indented under it, every line naming Dana.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part most demos skip: it's not fixed in advance
&lt;/h2&gt;

&lt;p&gt;It would be easy to fake this with a script — make the agent call the tools in a set order and the audit comes out clean because nothing could have gone another way. That proves the plumbing, not governance. So the showcase takes the strings off: a real model is handed the agent's actual capabilities as tools and &lt;strong&gt;decides, round by round&lt;/strong&gt;, what to call. The receipt above is from that loop — and when the agent reaches past its grant, the door, not a script, is what stops it.&lt;/p&gt;

&lt;p&gt;No new door was needed for the model to drive this. The model door authorizes only the logical model name, rewrites it to the provider's, and forwards the rest of the tool-calling conversation as-is. An agentic loop is just more governed model calls plus whichever doors the model chose — each a first-hand event. The agent is untrusted; the doors govern it regardless.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three honest limits (because the trust is in the limits)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;This showcase does not prove containment.&lt;/strong&gt; The compartments here are host processes started by test stand-ins; nothing in &lt;em&gt;this&lt;/em&gt; run proves the agent couldn't reach a provider on its own. Those walls are the operator's runtime's job, and the project keeps &lt;strong&gt;separate, podman-based proofs&lt;/strong&gt; for them (real rootless containers, &lt;code&gt;--network none&lt;/code&gt;, checked for no egress). I'll walk through that in a later post. I'm not going to hand-wave it here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't promise an outcome.&lt;/strong&gt; Real models vary — a run may skip a door, call one twice, take another path. The promise is narrower and testable: &lt;em&gt;whatever the model did was governed and is on the ledger.&lt;/em&gt; The deterministic proof of the loop itself is a hermetic CI run that plays a fixed tool-calling script against the same doors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The agent's own summary is not evidence.&lt;/strong&gt; The agent returns a tidy "here's what I did" artifact; it's useful for reading along and worthless as proof, because the untrusted agent wrote it. The evidence is the &lt;strong&gt;door events&lt;/strong&gt;, written first-hand by Agenthof and the runtimes. If the two ever disagreed, the ledger is what happened. And the ledger is &lt;strong&gt;tamper-evident, not tamper-proof&lt;/strong&gt; — the docs say exactly where that line is. No "tamper-proof," no sandboxed magic.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Run it and read your own receipt
&lt;/h2&gt;

&lt;p&gt;Everything above is in the repo under Apache-2.0 — the engine, the agent, the showcase, the hermetic CI run, and the honest-limits page. Two ways to see it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# hermetic — no API key, no network, no containers; a fixed model script&lt;/span&gt;
./scripts/e2e-acceptance-llm-local.sh

&lt;span class="c"&gt;# real model — bring your own OpenAI-compatible key; the model decides&lt;/span&gt;
&lt;span class="nv"&gt;SHOWCASE_PROVIDER_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-... ./scripts/showcase-combined.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both print &lt;code&gt;agenthof audit &amp;lt;run&amp;gt;&lt;/code&gt;, then &lt;code&gt;audit verify&lt;/code&gt; on the parent and every sub-agent, then wipe their working directory. Your key is stripped from the environment before any process starts and reaches only the host &lt;code&gt;agenthof&lt;/code&gt; process, per command — the agent never sees it.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;&lt;a href="https://github.com/agenthof/agenthof" rel="noopener noreferrer"&gt;Agenthof&lt;/a&gt;&lt;/strong&gt; — an open-source governance plane that sits &lt;em&gt;beside&lt;/em&gt; your IdP and your agent framework. It's early, and I'm honest about its limits. If part 1 made you wince and this made you curious, clone it, run the showcase, and read a receipt you didn't have to trust me for — the repo's &lt;a href="https://github.com/agenthof/agenthof" rel="noopener noreferrer"&gt;here&lt;/a&gt;, and a star helps the people who feel this find it. Next post: three identities for every agent action — who asked, what ran, whose key — and why delegated identity beats a shared bot.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>llm</category>
      <category>security</category>
    </item>
    <item>
      <title>Your AI agents are running as god-mode service accounts</title>
      <dc:creator>Rojaneer</dc:creator>
      <pubDate>Tue, 13 Oct 2026 12:31:21 +0000</pubDate>
      <link>https://dev.to/rojaneerdev/your-ai-agents-are-running-as-god-mode-service-accounts-iih</link>
      <guid>https://dev.to/rojaneerdev/your-ai-agents-are-running-as-god-mode-service-accounts-iih</guid>
      <description>&lt;p&gt;Your support copilot just updated a customer record. Your ops agent just restarted a service. Your coding agent just opened a pull request.&lt;/p&gt;

&lt;p&gt;Now answer the one question your auditor — or your incident review, or your own 2 a.m. self — is going to ask:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who authorized that? Which &lt;em&gt;human&lt;/em&gt;, acting through which &lt;em&gt;agent&lt;/em&gt;, with &lt;em&gt;whose&lt;/em&gt; permissions?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're like most teams shipping agents right now, you can't answer cleanly. Not because you were careless — because the tools you wired together were never designed to answer it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways we wire agent access today — and why each one fails the question
&lt;/h2&gt;

&lt;p&gt;When an agent needs to &lt;em&gt;do&lt;/em&gt; something in a real system, its credentials get set up one of three ways. I've done all three. They all leave the same hole.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. A shared service account.&lt;/strong&gt; One bot identity behind every agent action. Simple, and it works — until you look at the log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-10-13T09:14:22Z  action=record.update  actor=svc-agent-bot  resource=crm/contacts/8812
2026-10-13T09:14:48Z  action=service.restart actor=svc-agent-bot  resource=prod/api-gateway
2026-10-13T09:15:03Z  action=pr.create       actor=svc-agent-bot  resource=repo/payments
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every line says &lt;code&gt;svc-agent-bot&lt;/code&gt;. You've traded per-human accountability for a single god-mode key that half your org can trigger. When something goes wrong, the trail stops at the robot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A credential per agent.&lt;/strong&gt; Better isolation — now a rogue agent can't do &lt;em&gt;everything&lt;/em&gt;. But you've also just created N long-lived secrets to store, rotate, and pray never leak; the agent process &lt;em&gt;holds&lt;/em&gt; the raw credential; and the action is still attributed to &lt;em&gt;the agent&lt;/em&gt;, not to the human who asked for it. "The invoicing-agent did it" is not an answer to "who authorized it."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. "We use our IdP."&lt;/strong&gt; You give the agent an identity in Okta or Entra. This is the right instinct — and it's where most "agent identity" stories stop. But issuing an identity is not the same as governing what the agent &lt;em&gt;does&lt;/em&gt; with it. Your IdP hands the agent a token and steps out of the way. It does not sit in the path of the tool call or the shell command and &lt;em&gt;refuse&lt;/em&gt; one. And it does not produce an independent record of &lt;strong&gt;what the agent did with that token, on whose behalf, and what it was denied.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The hole, drawn
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqlffu3f25fyl035gpswy.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%2Fqlffu3f25fyl035gpswy.png" alt=" " width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The left side (identity) and the right side (the work) both have owners. &lt;strong&gt;The middle question has no owner.&lt;/strong&gt; That's the gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the obvious candidates don't fill it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The agent platforms won't.&lt;/strong&gt; LangChain, Langflow, n8n, and friends exist to help you &lt;em&gt;build and run&lt;/em&gt; agents. Governance shows up as a bolt-on feature, and — reasonably — they're not in the business of being a &lt;em&gt;neutral&lt;/em&gt; producer of evidence about their own runs. Asking the thing that runs the agent to be the impartial record of what the agent did is a conflict of interest, not an architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The IdPs can't.&lt;/strong&gt; Identity issuance is genuinely hard and genuinely valuable, and the IdPs are good at it. But they live at the &lt;em&gt;edge&lt;/em&gt; — they mint the token and go home. They are not in your tool calls or your exec commands at runtime, so they can't refuse one, and they can't record the thing that matters: the &lt;em&gt;action&lt;/em&gt;, not just the &lt;em&gt;login&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;So the question falls between two well-funded stools. Which is exactly why, if you've shipped an agent that touches anything real, you've felt the hole even if you couldn't name it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "answering the question" would actually require
&lt;/h2&gt;

&lt;p&gt;Forget products for a second. If you were going to design this honestly, the layer that owns the middle would need four properties — none of them exotic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Three identities on every action, not one:&lt;/strong&gt; &lt;em&gt;who asked&lt;/em&gt; (the human), &lt;em&gt;what ran&lt;/em&gt; (the agent), and &lt;em&gt;whose key acted&lt;/em&gt; (the resource) — delegated, so the human is on the wire, not a shared bot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The agent never holds the raw credential.&lt;/strong&gt; It asks; something trusted brokers a short-lived, scoped token; the secret never enters the agent's process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refusal happens in the call path, by construction&lt;/strong&gt; — not as an after-the-fact alert, and not as a promise the agent will behave.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every action &lt;em&gt;and every refusal&lt;/em&gt; lands in a tamper-evident record&lt;/strong&gt; your GRC team can actually read — one that doesn't depend on trusting the agent platform or any single IdP.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice what's &lt;em&gt;not&lt;/em&gt; on that list: a shiny chat UI, a drag-and-drop builder, an LLM deciding what's allowed. The hard, unglamorous part is the evidence and the enforcement — the boring governance middle that nobody's incentivized to own.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is a real, unsolved gap
&lt;/h2&gt;

&lt;p&gt;Not a hypothetical. Agents are already acting in production systems with shared keys and no per-human trail, and the two layers best positioned to fix it each have a structural reason not to. That's usually where a new, narrow layer belongs.&lt;/p&gt;

&lt;p&gt;That gap is what I'm building an open-source layer for — &lt;strong&gt;&lt;a href="https://github.com/agenthof/agenthof" rel="noopener noreferrer"&gt;Agenthof&lt;/a&gt;&lt;/strong&gt;, Apache-2.0, a governance plane that sits &lt;em&gt;beside&lt;/em&gt; your IdP and your agent framework rather than replacing either. It's early and I'm honest about its limits. In the &lt;strong&gt;next post&lt;/strong&gt; I'll show the thing working: an agent acting &lt;em&gt;as me&lt;/em&gt; in a real system, unable to exceed its grant, with the audit trail that proves it. If the question at the top of this post made you wince, the repo's &lt;a href="https://github.com/agenthof/agenthof" rel="noopener noreferrer"&gt;here&lt;/a&gt; — a star helps me find the people who feel this.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>cybersecurity</category>
      <category>security</category>
    </item>
    <item>
      <title>Let the model decide — then read the audit</title>
      <dc:creator>Rojaneer</dc:creator>
      <pubDate>Sun, 04 Oct 2026 05:18:31 +0000</pubDate>
      <link>https://dev.to/rojaneerdev/let-the-model-decide-then-read-the-audit-7g5</link>
      <guid>https://dev.to/rojaneerdev/let-the-model-decide-then-read-the-audit-7g5</guid>
      <description>&lt;p&gt;Most "governed agent" demos are governed because the demo author pulled every string. The agent calls the tool the script says, in the order the script says, and the audit comes out clean because nothing could have gone another way. That proves the plumbing. It does not prove governance, because governance is what happens when the thing you are governing has a will of its own.&lt;/p&gt;

&lt;p&gt;So we took the strings off.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/agenthof/agenthof" rel="noopener noreferrer"&gt;Agenthof&lt;/a&gt; is a small control plane for AI agents with one rule: an agent gets capability only through doors — model, tool, exec, spawn — and each door authorizes the call, injects the credential the agent never holds, and writes a ledger event bound to the human who started the run. The reference agent (LangChain, Python) used to drive those doors from a fixed script. Now a real model is handed the agent's actual capabilities as tools — the exec door, the spawn door, and exactly the tools its grant mirrors — and decides, round by round, what to call.&lt;/p&gt;

&lt;p&gt;No new door was needed. The model door authorizes only the logical model name, rewrites it to the provider's, and forwards the rest of the tool-calling conversation as it is. So an agentic loop is just more governed model calls, plus whichever exec, tool and spawn calls the model chose, each a first-hand event of its own. The agent is untrusted; the doors govern it regardless.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the audit shows
&lt;/h2&gt;

&lt;p&gt;Run it against your own OpenAI-compatible provider. The script prints &lt;code&gt;agenthof audit &amp;lt;run&amp;gt;&lt;/code&gt; for the parent run. Its header says &lt;code&gt;ledger integrity: verified (N events)&lt;/code&gt;; below it, in order: a &lt;code&gt;model fast — N prompt / M completion tokens&lt;/code&gt; line per round the model took; an &lt;code&gt;exec&lt;/code&gt; event the operator's runtime attested first-hand; a &lt;code&gt;tool&lt;/code&gt; call made &lt;em&gt;as the human&lt;/em&gt; through a token exchange, and another through a bridge runtime that attests it ran the tool; and two &lt;code&gt;spawn&lt;/code&gt; events, each a child run with its own ledger, linked to the parent and bound to the same person.&lt;/p&gt;

&lt;p&gt;If the model asked for something it was not granted, there is a recorded refusal. When the exec or spawn door refuses, the agent tells the model &lt;code&gt;refused by policy&lt;/code&gt;; when the tool door refuses, the model is told &lt;code&gt;the door call failed&lt;/code&gt;. Either way the model decides what to do next, and the step may still succeed. A refused model call instead fails the step — there is no model to ask. Against a real provider that is the likeliest refusal: a rate limit is recorded as &lt;code&gt;model fast refused — budget&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;agenthof investigate --run &amp;lt;run&amp;gt;&lt;/code&gt; then prints the whole tree as one timeline, each child's events indented under the parent's. The script runs &lt;code&gt;audit verify&lt;/code&gt; on the parent and each child, and removes its working directory on exit. Your key is removed from the environment before any process starts and reaches the host &lt;code&gt;agenthof&lt;/code&gt; process per command only; the agent never sees it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does not show
&lt;/h2&gt;

&lt;p&gt;Three honest limits.&lt;/p&gt;

&lt;p&gt;It does not show containment. The compartments here are host processes started by test stand-ins. Whether an agent &lt;em&gt;could&lt;/em&gt; reach a provider on its own is the operator's runtime's job; the project keeps separate, podman-based proofs for that.&lt;/p&gt;

&lt;p&gt;It does not promise an outcome. Real models vary; a run may skip a door, call one twice, or take another path. The promise is narrower: whatever the model did was governed and is on the ledger. The deterministic proof of the loop is a hermetic CI run that plays a fixed tool-calling script against the same doors.&lt;/p&gt;

&lt;p&gt;And the agent's own report is not evidence. The step artifact — &lt;code&gt;exec: …&lt;/code&gt;, &lt;code&gt;tool whoami: …&lt;/code&gt; — is the untrusted agent summarizing itself. The evidence is the door events, written by Agenthof and the runtimes. If they ever disagreed, the ledger is what happened. The ledger is append-only and hash-chained: it catches an edit, deletion or reordering that does not recompute every later hash. It is tamper-evident within those limits, not tamper-proof, and the docs say exactly where the limits are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Any framework
&lt;/h2&gt;

&lt;p&gt;The agent is two layers: a framework-neutral door layer — plain HTTP and MCP against a published wire contract, which also defines the two door tools and discovers the granted tools — and a thin LangChain shell that binds those tools and carries each model turn. Porting to another framework means rewriting the shell. The doors do not know or care which framework is talking. First-class support for agents built on other runtimes and frameworks is on the roadmap as "More harness adapters"; the point of this one is that it needed no special treatment.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/agenthof/agenthof/blob/main/docs/showcase.md" rel="noopener noreferrer"&gt;showcase&lt;/a&gt;, the hermetic CI run, and the honest-limits page are in the &lt;a href="https://github.com/agenthof/agenthof" rel="noopener noreferrer"&gt;repo&lt;/a&gt; under Apache-2.0 (tagged &lt;a href="https://github.com/agenthof/agenthof/releases/tag/v0.1.0" rel="noopener noreferrer"&gt;v0.1.0&lt;/a&gt;). Bring a key, run it, and read the audit — then decide for yourself what it proves.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>llm</category>
      <category>podman</category>
    </item>
    <item>
      <title>'No tools listed' should mean deny, not allow-all</title>
      <dc:creator>Rojaneer</dc:creator>
      <pubDate>Mon, 28 Sep 2026 04:08:41 +0000</pubDate>
      <link>https://dev.to/rojaneerdev/no-tools-listed-should-mean-deny-not-allow-all-9e7</link>
      <guid>https://dev.to/rojaneerdev/no-tools-listed-should-mean-deny-not-allow-all-9e7</guid>
      <description>&lt;p&gt;Here's a config bug that has shipped in more systems than anyone would like to admit:&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="c1"&gt;# grant this service access to... nothing? everything?&lt;/span&gt;
&lt;span class="na"&gt;allowedOrigins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a surprising number of frameworks, an empty or missing list doesn't mean "no access." It means all access. Empty CORS origins → reflect any origin. A Kubernetes pod with no NetworkPolicy selecting it → all traffic allowed. A firewall with the rules commented out → wide open. The pattern is everywhere, and it has a name worth calling out: &lt;strong&gt;privilege by omission&lt;/strong&gt; — the widest possible access, granted not by a decision but by the absence of one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why omission is the dangerous default
&lt;/h2&gt;

&lt;p&gt;The danger isn't that someone types "allow everything." Someone typing that has made a choice you can see in code review. The danger is that &lt;em&gt;nobody typed anything&lt;/em&gt;, and the system filled the silence with maximum privilege.&lt;/p&gt;

&lt;p&gt;That happens constantly, and invisibly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A refactor renames a field. The old key is now ignored, silently falling back to the default — which is "allow all."&lt;/li&gt;
&lt;li&gt;A merge drops a line from a list. The list is now empty — which is "allow all."&lt;/li&gt;
&lt;li&gt;Someone copies a config skeleton, means to fill in the allowlist later, and forgets. The skeleton is "allow all."&lt;/li&gt;
&lt;li&gt;A typo in the key name (&lt;code&gt;allowedTools&lt;/code&gt; vs &lt;code&gt;allowed_tools&lt;/code&gt;) means the field you carefully wrote is never read. What is read is the default: "allow all."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of these looks fine in review. The config reads as intentional. There's no red diff line saying "grant everything" — there's just... less config than there should be. And less config quietly resolved to more power.&lt;/p&gt;

&lt;p&gt;Contrast that with the same mistakes under a deny-by-default rule: the refactor, the dropped line, the forgotten allowlist all resolve to &lt;em&gt;less&lt;/em&gt; access. The agent or service suddenly can't do its job, someone notices in about five minutes, and they fix it. A too-small grant is a bug report. A too-large grant is an incident you find out about later, from someone else.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule: name your capabilities, or be rejected
&lt;/h2&gt;

&lt;p&gt;The fix is two rules applied together:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The widest access must be an explicit marker you typed.&lt;/strong&gt; "Everything" is a real, legitimate choice — but it should &lt;em&gt;look&lt;/em&gt; like a choice: a literal &lt;code&gt;["*"]&lt;/code&gt;, a &lt;code&gt;mode: all&lt;/code&gt;, something a reviewer can see and question. Never something you back into by leaving a field blank.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Naming no capabilities at all is an error — reject it, don't default it.&lt;/strong&gt; This is the part people skip. It's tempting to say "empty list = deny all, safe default, done." But an empty allowlist is almost never what someone &lt;em&gt;meant&lt;/em&gt;; it's what they got when something went wrong. Silently denying everything hides the mistake (now you're debugging why the agent won't work). Silently allowing everything is a hole. Rejecting it at load surfaces the mistake immediately, while the person who made it is still looking at the file.&lt;/p&gt;

&lt;p&gt;Call it &lt;strong&gt;no privilege by omission&lt;/strong&gt;: fail closed, and fail loud. The ambiguous state — "a grant that names nothing" — shouldn't resolve to a guess in either direction. It shouldn't be representable as a running config at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  A worked example
&lt;/h2&gt;

&lt;p&gt;I build Agenthof, an open-source (Apache-2.0, Go) governance layer for AI agents, and we made this a rule the whole system is held to. From its constitution:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A grant that names a set of reachable capabilities — invokers, tools, executables — declares which ones; the widest such set is an explicit marker (a role's &lt;code&gt;["*"]&lt;/code&gt;, a tool grant's &lt;code&gt;tools: ["*"]&lt;/code&gt;), and a grant that names none is rejected at &lt;code&gt;apply&lt;/code&gt;, never widened to the maximum.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Concretely, a tool grant used to allow a bare string as shorthand for "every tool this server exposes":&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;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;my-mcp&lt;/span&gt;          &lt;span class="c1"&gt;# retired: used to mean EVERY tool my-mcp exposes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's exactly privilege by omission — the maximum, granted by writing the least. So it was retired. Every-tool access now has to say so — the &lt;code&gt;["*"]&lt;/code&gt; marker on the &lt;em&gt;tools&lt;/em&gt; axis (the same token a role uses for groups), plus an explicit &lt;code&gt;mode&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-mcp&lt;/span&gt;
    &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&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="pi"&gt;]&lt;/span&gt;              &lt;span class="c1"&gt;# every tool this server exposes...&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read-write&lt;/span&gt;          &lt;span class="c1"&gt;# ...and may mutate — you stated both axes&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;billing-mcp&lt;/span&gt;
    &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;get_invoice&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;      &lt;span class="c1"&gt;# or name exactly the tools you want&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read-only&lt;/span&gt;           &lt;span class="c1"&gt;# and whether they may change anything&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a grant that names neither specific tools nor a mode is rejected when the config is loaded — with an error that tells you how to fix it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bad-tool-grant: line 6: a bare tool grant is no longer accepted;
a tool grant needs tools and mode: write {resource: "my-mcp",
tools: ["*"], mode: read-write} for every tool, or list tools and set mode
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same rule holds across the other capability lists, not just tools. A role opens to everyone only via an explicit &lt;code&gt;allowed_groups: ["*"]&lt;/code&gt;; a role that declares no access floor at all is rejected, never treated as open. Executable allowlists work the same way. The point isn't the specific syntax — it's that in every place the config names a set of capabilities, "I named none" is a compile-time-style error, not a runtime surprise.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to apply it to your own configs
&lt;/h2&gt;

&lt;p&gt;You don't need a governance framework for this. Next time you design a config that grants access, for each list-shaped permission ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What does empty/missing mean here — deny-all or allow-all?&lt;/strong&gt; If you can't answer instantly, neither can the next person to edit it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make "everything" an explicit sentinel&lt;/strong&gt; (&lt;code&gt;["*"]&lt;/code&gt;, &lt;code&gt;all&lt;/code&gt;, a named flag) — never the value you get by leaving the field out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reject the empty/missing case at load time&lt;/strong&gt;, with an error that names the two legitimate fixes (a real list, or the explicit-all marker). Fail loud, not into a default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate before anything runs&lt;/strong&gt;, so the mistake is caught at apply/startup — not discovered in production when an agent does something no one scoped it to.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One honest caveat: default-deny only buys you anything if the thing enforcing it actually consults the config on &lt;em&gt;every&lt;/em&gt; access. A beautifully strict allowlist that some code path skips is decoration. Deny-by-default is a property of the chokepoint, not the YAML.&lt;/p&gt;

&lt;p&gt;But get the chokepoint right, and this small discipline pays off constantly: the worst thing a dropped line, a typo, or a forgotten TODO can do to your permissions is grant too &lt;em&gt;little&lt;/em&gt;. And too little is a bug someone reports on Monday — not a breach someone reports on your behalf.&lt;/p&gt;

&lt;p&gt;If you want to see the rule enforced end-to-end, it's all in the open at &lt;a href="https://github.com/agenthof/agenthof" rel="noopener noreferrer"&gt;github.com/agenthof/agenthof&lt;/a&gt;. And if you've hit the empty-list-means-everything trap in the wild, I'd love to hear where — I'm collecting them.&lt;/p&gt;

</description>
      <category>security</category>
      <category>go</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Keep your API keys out of your AI agent: a credential pattern for MCP servers</title>
      <dc:creator>Rojaneer</dc:creator>
      <pubDate>Sun, 27 Sep 2026 11:08:31 +0000</pubDate>
      <link>https://dev.to/rojaneerdev/keep-your-api-keys-out-of-your-ai-agent-a-credential-pattern-for-mcp-servers-4cmo</link>
      <guid>https://dev.to/rojaneerdev/keep-your-api-keys-out-of-your-ai-agent-a-credential-pattern-for-mcp-servers-4cmo</guid>
      <description>&lt;p&gt;If you've wired an MCP server into an agent, you've probably done something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"billing"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"billing-mcp"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"BILLING_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sk-live-..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works. It's also handing your live billing key to the least trustworthy process in the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;An agent is a program that decides what to do at runtime based on text it was given — some of which comes from the outside world (a webpage it read, a document it summarized, a tool result). That's the whole point, and it's also why the agent process is the wrong place to keep a secret.&lt;/p&gt;

&lt;p&gt;Two things go wrong with the config above:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The credential lives in the agent's environment.&lt;/strong&gt; If the agent is compromised — prompt injection, a poisoned dependency, a tool that returns a malicious payload — the attacker is now one &lt;code&gt;os.environ&lt;/code&gt; read away from your billing key. The blast radius of "the agent did something dumb" includes "the agent's keys are gone."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The agent can call everything, and you can't prove what it did.&lt;/strong&gt; The MCP server exposes a set of tools; the agent can call any of them. When something goes wrong, your evidence is scattered across logs that the agent itself could have influenced.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can't fix this by making the agent more careful. The agent is the untrusted part. You fix it by moving the trust boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern: put a gateway between the agent and the MCP server
&lt;/h2&gt;

&lt;p&gt;Instead of letting the agent talk to the MCP server directly, put a small trusted process — a gateway — in the middle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;agent  ──►  gateway  ──►  MCP server
           (holds the       (needs the
            credential)      credential)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;credential lives in the gateway's environment&lt;/strong&gt;, not the agent's.&lt;/li&gt;
&lt;li&gt;The agent gets a &lt;strong&gt;short-lived token scoped to its current run&lt;/strong&gt;, and a URL that only reaches the MCP server &lt;em&gt;through&lt;/em&gt; the gateway.&lt;/li&gt;
&lt;li&gt;On each tool call, the gateway &lt;strong&gt;injects the real credential&lt;/strong&gt; just before forwarding upstream, and strips it from anything it hands back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent is &lt;em&gt;starved&lt;/em&gt; of credentials. It can make tool calls, but it never possesses the secret that authorizes them. Compromise the agent and you get a revocable, per-run token — not the billing key.&lt;/p&gt;

&lt;p&gt;This is more than a reverse proxy, because the gateway is a policy decision point. Since every call goes through it, it can also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;enforce &lt;strong&gt;which tools&lt;/strong&gt; each agent may call (default-deny),&lt;/li&gt;
&lt;li&gt;enforce &lt;strong&gt;read-only&lt;/strong&gt; access where you want it,&lt;/li&gt;
&lt;li&gt;and &lt;strong&gt;record every call and every refusal&lt;/strong&gt; in one place the agent can't rewrite.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A worked example
&lt;/h2&gt;

&lt;p&gt;Here's the pattern implemented with &lt;a href="https://github.com/agenthof/agenthof" rel="noopener noreferrer"&gt;Agenthof&lt;/a&gt;, an open-source (Apache-2.0, Go) governance gateway. The config is the useful part; the tool is just one way to run it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Register the MCP server with the gateway — this is where the secret lives:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ticket-search&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mcp&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://tickets.internal/mcp&lt;/span&gt;
    &lt;span class="na"&gt;credential_source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;static_env&lt;/span&gt;
    &lt;span class="na"&gt;token_env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TICKETS_MCP_TOKEN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;TICKETS_MCP_TOKEN&lt;/code&gt; is read from the &lt;em&gt;gateway's&lt;/em&gt; environment. The agent process never sees it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Grant each agent only the tools it needs — default-deny:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;legacy-triage&lt;/span&gt;
&lt;span class="na"&gt;execution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fronted&lt;/span&gt;
&lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://legacy.internal/agents/triage&lt;/span&gt;
&lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ticket-search&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;                            &lt;span class="c1"&gt;# every tool ticket-search exposes&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;billing-mcp&lt;/span&gt;
    &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;get_invoice&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;list_invoices&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# only these two of billing-mcp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An agent gets access to a resource only if it's named here, and to the &lt;em&gt;tools&lt;/em&gt; it names (or an explicit &lt;code&gt;mode: all&lt;/code&gt;). Leaving the list off isn't "allow everything" — it's rejected at config load. Widest access is always something you typed on purpose, never something you got by omission.&lt;/p&gt;

&lt;p&gt;Want read-only? Say so, and the operator's classification decides what counts:&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;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;billing-mcp&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read-only&lt;/span&gt;        &lt;span class="c1"&gt;# only the tools billing-mcp is declared to expose read-only&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. For OAuth-protected servers, even the client secret stays out of the agent.&lt;/strong&gt; The gateway's broker mints a token from your identity provider per call:&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;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;billing-mcp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mcp&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://billing.internal/mcp&lt;/span&gt;
    &lt;span class="na"&gt;credential_source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;static_env&lt;/span&gt;
    &lt;span class="na"&gt;grant_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;client_credentials&lt;/span&gt;
    &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://idp.example.com/&lt;/span&gt;
    &lt;span class="na"&gt;token_endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://idp.example.com/oauth2/token&lt;/span&gt;
    &lt;span class="na"&gt;client_id_env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;BILLING_MCP_CLIENT_ID&lt;/span&gt;
    &lt;span class="na"&gt;client_secret_env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;BILLING_MCP_CLIENT_SECRET&lt;/span&gt;
    &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;billing.read&lt;/span&gt;
    &lt;span class="na"&gt;read_only_tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;get_invoice&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;list_invoices&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client id and secret are read from the gateway's environment; the agent gets the minted access token's &lt;em&gt;effects&lt;/em&gt;, never the token, and never the secret that mints it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Because everything flows through one point, every tool call and every refusal lands in a hash-chained, tamper-evident audit ledger&lt;/strong&gt; — so "what did this agent actually do?" has a single, ordered answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does &lt;em&gt;not&lt;/em&gt; do
&lt;/h2&gt;

&lt;p&gt;Being honest about the boundary is the whole point, so:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;This governs and audits. It does not &lt;em&gt;contain&lt;/em&gt; the agent.&lt;/strong&gt; A compromised agent process still runs on some host; if that host has no sandbox, the process can still do local damage or reach the network directly. Keeping credentials out of the agent shrinks the blast radius — it is not a jail. Containment is your sandbox's job; this is the keys and the CCTV, not the locked room.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The ledger is tamper-&lt;em&gt;evident&lt;/em&gt;, not tamper-&lt;em&gt;proof&lt;/em&gt;.&lt;/strong&gt; Hash-chaining means edits are detectable after the fact. It does not stop an attacker with enough privilege from rewriting history — it makes the rewrite show up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those aren't weaknesses to paper over; they're where the boundary actually sits, and knowing that is how you deploy the pattern correctly (gateway on a trusted host, agent in a sandbox, ledger shipped somewhere append-only).&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Regardless of which tool you use, the pattern is portable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Secrets live in a trusted gateway, never in the agent.&lt;/strong&gt; The agent gets a scoped, revocable, per-run token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Default-deny tool access.&lt;/strong&gt; An agent reaches a tool only because you named it, and read-only where read-only is enough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log every call and refusal&lt;/strong&gt; somewhere the agent can't edit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent stays the flexible, fallible thing it's supposed to be — and it stops being the thing that holds your keys.&lt;/p&gt;

&lt;p&gt;A working implementation of all of the above is at &lt;a href="https://github.com/agenthof/agenthof" rel="noopener noreferrer"&gt;github.com/agenthof/agenthof&lt;/a&gt;. If you're doing this differently — a sidecar, a service mesh, provider-side scoping — I'd genuinely like to hear how it's holding up.&lt;/p&gt;

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