From god-mode to governed: an AI agent that can’t exceed its grant
Last time I argued that AI agents are running as god-mode service accounts: an agent acts in your systems, and nobody can cleanly answer who authorized that — which human, acting through which agent, with whose permissio
Last time I argued that AI agents are running as god-mode service accounts: an agent acts in your systems, and nobody can cleanly answer who authorized that — which human, acting through which agent, with whose permissions, and was anything refused? I promised the next post would show the thing working.
So here it is: a real agent, acting as me, 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.
The setup: two programs, one rule
There are two programs here, and keeping them separate is the whole point.
-
agenthofis the governance engine —apply,run,audit,investigate. It never embeds the agent or decides how it thinks. -
The agent is a separate program (a small LangChain agent in the repo).
agenthofcalls it over a socket and drives it through doors: 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.
I start the run as a verified human — in the showcase a stub IdP issues Dana's token; in production your own IdP (Okta, Entra, Keycloak) mints it and agenthof serve verifies it per call. Either way, from that point on, Dana is on the wire, not a shared bot.
The agent runs with no network and no secret of its own. 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 RFC 8693 token exchange — swapping Dana's verified token for one scoped to that upstream — and calls the system as Dana. The secret never enters the agent's process.
Read the receipt
After the run, agenthof audit <run> prints the ledger for that run, in order. This is the receipt. Here it is from an actual hermetic run of scripts/e2e-acceptance-llm-local.sh — I've elided the leading timestamps, a temp path, and the tails of run ids for width; nothing else is changed:
$ agenthof audit r-85f417c68560e228
run r-85f417c68560e228 — acceptance (role acceptance-operator)
invoked by [email protected] (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
Map it to the four questions from part 1:
-
Which human?
invoked by [email protected] (oidc, issuer …)— stamped on the run, and every event belongs to it. Notsvc-agent-bot. (In this hermetic run a stub issues Dana's token, so the issuer is a localhost address; in production it's your IdP.) -
Which agent, and what did it actually do? The
model fast,exec,tool, andspawnlines — each written first-hand by Agenthof or the operator's runtime ([runtime-attested: refexec …],[runtime-attested: refbridge …]), not by the agent. Note what's not there: no prompt text, no completion text, and themodel fastline doesn't even reveal which tool the model asked for. The model's choices show up only as the door events they became. -
Whose key acted?
tool whoami — … (token_exchange). The agent called a per-user tool, but the gateway exchanged Dana's token (RFC 8693) and called it as Dana — the step's own summary comes backtool whoami: acting as: u-dana. The agent held only its run token; it never saw a secret. - Was anything refused? Not in this 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:
$ agenthof audit r-c5cc1961… # same workflow, stricter allowlist
…
exec env refused — command is not on the exec allowlist
…
workflow finished: succeeded
The door said no and recorded the no. The agent was told refused by policy, the model moved on, and the step still finished — 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.)
And above all of it: ledger integrity: verified, and audit verify on each run returns the chain head:
$ agenthof audit verify r-85f417c6…
head: 4d655340… (13 events)
The ledger is append-only and hash-chained, so it catches an edit, deletion, or reordering that doesn't recompute every later hash. agenthof investigate --run <run> 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.
The part most demos skip: it's not fixed in advance
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 decides, round by round, 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.
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.
Three honest limits (because the trust is in the limits)
-
This showcase does not prove containment. The compartments here are host processes started by test stand-ins; nothing in this 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 separate, podman-based proofs for them (real rootless containers,
--network none, checked for no egress). I'll walk through that in a later post. I'm not going to hand-wave it here. - It doesn't promise an outcome. Real models vary — a run may skip a door, call one twice, take another path. The promise is narrower and testable: whatever the model did was governed and is on the ledger. The deterministic proof of the loop itself is a hermetic CI run that plays a fixed tool-calling script against the same doors.
- The agent's own summary is not evidence. 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 door events, written first-hand by Agenthof and the runtimes. If the two ever disagreed, the ledger is what happened. And the ledger is tamper-evident, not tamper-proof — the docs say exactly where that line is. No "tamper-proof," no sandboxed magic.
Run it and read your own receipt
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:
# hermetic — no API key, no network, no containers; a fixed model script
./scripts/e2e-acceptance-llm-local.sh
# real model — bring your own OpenAI-compatible key; the model decides
SHOWCASE_PROVIDER_KEY=sk-... ./scripts/showcase-combined.sh
Both print agenthof audit <run>, then audit verify 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 agenthof process, per command — the agent never sees it.
This is Agenthof — an open-source governance plane that sits beside 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 here, 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.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.
