Interposed: Hardware-Rooted Enforcement for AI Agents, and auth-go
I have spent the last stretch of my life building Interposed, and alongside it auth-go, a Go implementation of the AAuth protocol. This post is a tour of both: what they do, how they fit together, and the design decisions underneath.
The problem
An AI coding agent with shell access can delete your repo, exfiltrate your SSH keys, or wipe a database. One bad prompt is enough — a poisoned issue comment, a malicious dependency README, a prompt injection buried in a web page the agent was asked to summarize.
Today’s tooling answers this in three ways, and none of them stop the syscall:
- Permission prompts inside the agent. The agent’s own harness asks “allow this command?” — but the process asking is the same process being manipulated. A compromised agent can be talked into approving, or into running something that looks harmless and isn’t.
- Sandboxes and containers. Useful, but coarse. Either the agent can’t reach the things it legitimately needs (your repo, your credentials, your deploy target) and becomes useless, or it can, and the blast radius is back.
- Post-hoc review. Logs and diffs tell you what happened after it happened. By the time you read the audit trail, the key is already exfiltrated.
The common failure is that every one of these controls lives above the action. Interposed sits below all of it: at the kernel, before the syscall runs.
What Interposed is
Interposed is a per-host Linux daemon that hooks the kernel via LSM-BPF and intercepts what an AI agent actually does — not what it says it will do. The enforcer attaches eight hooks (exec, file_open, socket_connect, ptrace, and more) over a watched PID tree: the agent process and everything it spawns.
Every intercepted action is checked against a trust list. One check, three outcomes:
- Trusted — the action matches the sealed trust list and passes with negligible overhead. Your agent stays fast for everything you’ve already decided is fine.
- Flagged — the action matches a quarantine rule. Hard stop: the syscall is denied and the offending process tree gets SIGKILL.
- Everything in between — the action blocks, and a human decides.
That third path is the heart of the system. When an agent reaches for something off the list, the enforcer holds the syscall and the policy broker — the AAuth Person Server, more on that below — fires a passkey ceremony. Nothing proceeds until a person physically touches a FIDO2 hardware key. The options at the prompt are deliberate:
- Approve — allow it once; ask again next time.
- Always trust — allow it and seal it into the trust list, so it never prompts again.
- Block — deny the syscall.
- Quarantine — deny it and SIGKILL the process tree.
The seal: why root can’t cheat
The obvious attack on any policy file is to edit the policy file. Agents are very good at editing files — that is their job. So the trust list is not a config file the way .bashrc is a config file.
The policy on disk is sealed to the hardware key using FIDO2 CTAP2 hmac-secret: deriving the key that authenticates the policy requires a physical touch on the token. An agent cannot rewrite its own permissions the way it edits a config file — and neither can root, because the secret needed to produce a valid seal never exists on the machine. It lives in the hardware key, and the hardware key lives on a human’s keychain.
This is vendor-agnostic: YubiKey, Nitrokey, SoloKeys, or any CTAP2-compliant key works. The property that matters is that the credential is non-exportable and touch-gated.
The intention layer
Enforcement tells you what the agent did. It cannot tell you why. So Interposed runs two planes:
- Intent plane (advisory). Agents that speak MCP declare what they are about to do before they do it. The declaration never enforces anything — it’s testimony, not authorization.
- Enforcement plane (mandatory). Every syscall, at the kernel, non-bypassable, regardless of what was declared.
The interesting signal is the diff between the two: an agent that declared “running the test suite” while opening ~/.ssh/id_ed25519 is telling you something worth interrupting.
On top of this sits the on-device intent oracle: a local model that annotates each approval request with the action’s likely intent, so the human at the ceremony decides with context rather than a raw syscall string. It runs entirely on the host — nothing leaves the machine. Approving connect 140.82.121.4:443 is guesswork; approving “push the branch you asked for to GitHub” is a decision.
Isolation for the untrusted
Some agents you gate; some you shouldn’t run on your host at all. For those, Interposed ships a custom Linux kernel image with bpf-lsm enabled plus a Firecracker microVM sandbox to isolate untrusted agents behind a sealed policy floor — the same enforcement, but with the agent’s entire world reduced to a microVM. It is driven from the same operator that runs host enforcement, so trusting an agent more (or less) is a policy change, not an infrastructure migration.
Audit that survives the machine
Every decision — pass, prompt, deny, quarantine — lands in a per-host Ed25519-signed audit log with a hash chain that survives restarts. Tampering with the record breaks the chain; deleting it is itself evidence.
When compliance needs the record off-box, the operator relays it to infrastructure you control, under a credential sealed to your hardware key:
- S3 with Object-Lock — verbatim NDJSON to any S3-compatible store (R2, MinIO, Backblaze B2, Wasabi, AWS) with a retention window you set. WORM: no rewrites.
- OpenTelemetry — severity-mapped logs straight into Splunk, Elastic, Datadog, or Grafana, so denials and quarantines land where responders already watch.
And because the deployment targets include regulated and classified networks: zero telemetry, no forced auto-update, offline license injection, releases shipped as sha256-checksummed artifacts. The whole thing runs air-gapped.
Operating a fleet
A single gated laptop is nice; a fleet is where policy earns its keep. The Interposed operator is a desktop app that manages many hosts over a mutually-authenticated Noise transport: per-machine capability and protection-tier probing (what can this kernel enforce? what is it currently enforcing?), tap-gated remediation (fixing a host’s posture is itself a ceremony), and a live embedded terminal for when you need hands on a box.
auth-go: the protocol layer
Underneath the broker is a protocol question that is bigger than Interposed: how does an AI agent prove who it is, and how does a human’s authority reach it? That question has a draft answer at the IETF — draft-hardt-oauth-aauth-protocol, the AAuth protocol — and auth-go is, to our knowledge, the first Go implementation of it (the draft’s implementation status lists TypeScript, .NET, Python, and Java).
AAuth involves four participants — an Agent making signed requests, a Resource (the protected API), a Person Server representing the user, and an Access Server enforcing federated policy — and stacks three layers: proving who the agent is, deciding what it may access, and governing what it is doing and why.
The parts I find most load-bearing:
- Agents get real cryptographic identity. Every agent holds its own Ed25519 key and a self-describing token that binds it:
aauth:[email protected]is an identity, not a config string. Sub-agents (name+worker@domain) are first-class, with a single-level rule and a policy hook for how a Person Server treats them. No shared secrets, no per-server pre-registration — any party can verify the token and every request it signs. - Requests are signed, not just authenticated. The transport implements the RFC 9421 HTTP Message Signatures profile (
@method @authority @path signature-keypluscontent-digest), so a token can’t be replayed against a different path or method. The test suite explicitly covers tampered-@pathand swapped-Signature-Keyrejection. - “A human is deciding” is a protocol state. Permission requests can return a deferred
202withLocationandRetry-After, and the client follows it automatically. This is exactly the shape of Interposed’s ceremony: the agent asks, the Person Server holds the request while a human reaches for their key, and the agent’s next poll gets the verdict. The draft even specifies a clarification chat — question, answer, updated request, cancel — for when the human needs more than approve/deny. - Trust is pluggable. The same verification code serves public JWKS discovery, pinned keys for offline and air-gapped deployments, or local self-signed agents — one
KeyResolverinterface, three worlds.
The API is small on purpose. An agent asking before acting is a few lines:
id, _ := aauth.ParseAgentIdentifier("aauth:[email protected]")
agent, _ := aauth.NewAgent(id, aauth.WithPersonServer("http://127.0.0.1:7421"))
ps := aauth.NewPSClient("http://127.0.0.1:7421", agent)
res, err := ps.RequestPermission(ctx, aauth.PermissionRequest{
Action: "WriteFile",
Description: "write the deploy config",
Parameters: map[string]any{"path": "/tmp/deploy.yaml"},
})
And for existing code, AAuth can disappear entirely behind an http.RoundTripper — the transport signs each request and turns 401 challenges into token exchanges:
hc := &http.Client{Transport: aauth.NewTransport(agent, ps)}
resp, err := hc.Get("https://files.example/files") // signed, challenged, retried
Coverage today: the Agent and Resource roles are fully implemented and tested (identity, token minting, all three access modes, call chaining, the act delegation chain); the Person Server side covers permission, token exchange, audit, clarification, and deferred responses, with the mission lifecycle pending; the four-party Access Server federation is planned. Everything is white-box tested against live httptest servers, MIT-licensed, matching the reference implementations.
How the pieces compose
The layering is the point:
- auth-go answers who is asking, and with whose authority — portable, standards-track, useful to anyone building agent infrastructure in Go, with or without Interposed.
- Interposed’s broker is an AAuth Person Server with opinions: its “deferred response” is a passkey ceremony, its policy store is sealed to a hardware key, its audit endpoint feeds a signed hash chain.
- The LSM-BPF enforcer is the backstop for agents that never asked. Well-behaved agents declare intent and request permission; the kernel gate is there for the ones that don’t.
Consent at the protocol layer, enforcement at the kernel, and a root of trust that lives on a keychain instead of a filesystem.
If you’re building agent infrastructure in Go, auth-go is on GitHub — issues and PRs welcome. And if you want a kill-switch for your agents, interposed.ai has an interactive simulation of the approval ceremony: an agent reaches for /etc/passwd, and you get to be the tap that says no.