Suresh Michael
All posts
AI Governance10 min read

Structurally Impossible: Policy Gates, Audit Trails, and the Limits of Agent Governance

An AI agent sends tool-call requests into a glowing policy gate marked with a shield and checkmark. Three paths leave the gate: a green tick allowing calls through to a database and a code service, a yellow path routing to a human reviewer with a clipboard, and a red cross blocking a destructive action at a warning sign. An audit checklist under a magnifying glass and a padlock sit in the foreground.
Three verdicts from one gate: allow, route to a human, deny

Architectural notes on Microsoft's Agent Governance Toolkit, and on the shape of the problem it is trying to solve.

The question always arrives late.

You have built the agent. It reads the CRM, drafts the reply, books the slot, escalates what it can't handle. It demos beautifully. Then someone from risk, or legal, or the client's audit function asks something that has nothing to do with the model: when it does the wrong thing, what stops it, and how do you prove afterwards what happened?

The usual answer is a paragraph in the system prompt. Sometimes a very carefully worded one.

Microsoft's Agent Governance Toolkit is MIT-licensed, in public preview, and exists because that answer is not an answer. I haven't run it in anger yet; what follows is a read of the specs, the source, and the parts of the documentation most projects would never publish. The library is interesting. The argument it makes is more interesting, and it survives independently of whether you ever install it.

1. Prompt-level safety is a request, not a control

Start with the claim the whole design rests on: instructions in a prompt are not a control surface.

The toolkit's README doesn't hedge about this, and it cites rather than asserts. OWASP's own guidance on prompt injection says plainly that "it is unclear if there are fool-proof methods of prevention." Andriushchenko et al. (ICLR 2025) report a 100% attack success rate against GPT-4o, GPT-3.5, Claude 3 and Llama-3 using adaptive attacks. Microsoft's own red-teaming write-ups land in the same place: mitigations reduce risk, they do not eliminate it, because model-layer defences are probabilistic by construction.

None of this means models are unsafe to deploy. It means a sentence like never delete production data is a probabilistic constraint on a stochastic system, and you cannot build an authorization system out of persuasion.

Readers of my voice platform notes will recognise the shape of this. SIP embeds IP addresses inside its payload, so you can either bolt on machinery that rewrites those addresses correctly under every condition, or you can design a topology where no address is ever wrong. The second is cheaper, and an entire class of bug simply stops existing.

Same move here. When behaviour is stochastic, spend your budget on making the unsafe action unavailable rather than on machinery that talks the model out of requesting it.

2. The design collapses to one decision: where the gate lives

Strip away the packages and the specifications and the toolkit makes exactly one architectural bet. Every tool call, message send and delegation is intercepted in deterministic application code, before the model's intent reaches anything real.

The minimal form is two lines:

from agentmesh.governance import govern

safe_tool = govern(my_tool, policy="policy.yaml")

With policy as data rather than code:

apiVersion: governance.toolkit/v1
default_action: allow
rules:
  - name: block-destructive
    condition: "action.type in ['drop', 'delete', 'truncate']"
    action: deny
    description: "Destructive operations require human approval"

  - name: require-approval-for-send
    condition: "action.type == 'send_email'"
    action: require_approval
    approvers: ["security-team"]

A denied call raises GovernanceDenied before the function body runs. The decision path is Agent → Policy Engine → Identity → Audit Log, the policy runtime is stateless and fail-closed with a Rust core, and each layer past the first is optional.

Here is the part I'd underline. There is nothing novel in any of this. It is authorization, the same discipline we have applied to human users and service accounts for thirty years, with the same primitives: a subject, a requested action, a policy, a verdict, a log. The only genuinely new fact is that the caller is non-deterministic.

That lack of novelty is the strongest thing about the design. An agent asking for something is free and unconstrained; the tool call is the only thing that touches the world. Put the control where the effect is, not where the intention is, and the model's creativity stops being a security property.

3. The audit trail is the part your client is actually buying

Teams evaluate this class of tool on blocking. Regulated clients buy it for evidence.

The audit layer records a decision, not an event: timestamp, agent identity, requested action, the policy that was active at the time, verdict, and denial reason. The trail is tamper-evident via Merkle hashing, with a Decision BOM and export paths aimed at SOC 2, NIST AI RMF and EU AI Act workstreams. That "policy active at the time" field is the one auditors care about, because it converts "we have governance" into a reconstructable claim about a specific Tuesday.

Two cautions, and I'd raise both in a client meeting.

A compliance mapping is a claim about your architecture, not a certificate. The repo maps controls to OWASP's Agentic Top 10, to NIST, to the EU AI Act. Those mappings are genuinely useful (they save weeks of drafting), but they are the start of an audit conversation. Nobody is certified by a table in a markdown file.

A decision log tells you what was permitted, not what happened. The project says so itself: the trail records attempts, not outcomes. An allowed action that hits an API, gets a 200, and returns stale garbage logs identically to one that worked. If you need outcome assurance, that is a separate mechanism: result validation in your own code, SLOs on action success, compensating transactions for multi-step work.

4. Take two layers and stop

The repository is large. Nine packages, a formal RFC 2119 specification per major component backed by 992 conformance tests, five language SDKs, a dozen-plus framework adapters, MCP tool-poisoning detection, shadow-agent discovery, chaos testing, and a Streamlit fleet dashboard.

The README's own advice is that most teams run policy enforcement plus audit logging and never need the rest. Believe it.

Mesh identity, privilege rings, trust scoring and error budgets are real answers to real problems, but to problems you should be able to name before you adopt the machinery. The cost is not just conceptual, either. The published sub-0.1ms benchmark measures the policy engine alone, which is honest and reproducible and also not what a distributed deployment experiences:

ComponentTypical latencyWhen it applies
Policy evaluation<0.1 msEvery action
Ed25519 signature verification1-3 msInter-agent messages
IATP handshake10-50 msFirst contact between two agents
Network round-trip1-10 msDistributed deployments only

Single process, the fast number is the whole story. Full mesh, it's 5-50ms per governed inter-agent interaction, dominated by cryptography and network rather than by policy. In a voice agent, where a human perceives the turn gap, 50ms per hop is budget you have to actually own.

Adopt the layer that answers a failure you can describe out loud. The rest is available later, and later is fine.

5. An in-process gate governs a confused agent, not a compromised one

This is the sharpest line in the documentation, and it is in the Security section where most readers skim: governance is enforced at the application middleware layer, not the OS kernel, and the policy engine and the agents share the same process boundary. The recommended production posture is one container per agent.

That distinction deserves more weight than a bullet point.

If your threat model is the model was steered into requesting something it shouldn't (indirect injection through a retrieved document, a manipulative user, plain confusion), then an in-process gate is exactly right. The check runs in code the model does not author, on a path it cannot skip.

If your threat model includes arbitrary code execution inside the agent process (and it should, the moment your agent runs generated code or loads a third-party MCP server), then a policy engine living in that same process is inside the blast radius. It is a control against the agent's intent, not against the agent's runtime.

A control is only as strong as the boundary it sits behind. Decide where the process boundary goes based on your threat model, and never let a library's presence in the import graph feel like isolation.

6. The gap that will bite you is composition

The most useful file in the repository is LIMITATIONS.md, and I mean that as praise. It is a list of things the toolkit does not do, published by the people selling it.

The central one: governance is per-action, and harm is frequently per-sequence.

If policy permits read_database and permits send_slack_message, an agent can read your customer list and post it to a public channel, and every individual step is compliant. The knowledge-flow variant is worse, because it looks even more innocent: retrieve a confidential HR document through an approved search tool, summarise it into an approved channel. Two green verdicts, one incident.

It extends across time, too. Where agents have persistent memory or persistent tools, attack state written in one session can be resumed in a later session whose permissions allow the next step; the chain only resolves across the sequence, so no single session ever looks wrong. The cited work (Dai et al., preprint) reports 80-95% success rates under that pattern.

Workflow-level policies and intent declaration are on the roadmap. They are not here.

What you can do today is stop writing policies as verb lists and start writing them as data-flow statements. Ask the only question that matters: which tools can read sensitive data, which tools can move data outward, and does any single agent hold both capabilities at once? That is answerable now, with per-agent policies and egress restrictions, and it is a design decision rather than a feature request.

Separate the readers from the senders. If one agent must do both, that agent is your control point and it needs a human in the loop, not a cleverer rule.

7. Public preview means design for replacement

The banner says public preview with possible breaking changes before GA, and the repository is honest about what that has already meant: a deprecation warning fires inside the quick-start import path, version 4.1.0 consolidated forty-five Python packages down to five, the v4-to-v5 policy migration is one-way, and there is a dedicated BREAKING_CHANGES.md.

That is not a criticism. It is a fast-moving project documenting its own churn, which is what you want. But it dictates how you adopt.

Put a seam in. Your application calls your own authorize(action, context); the toolkit lives behind it. The state is portable by design (policies are YAML, audit records are JSON, identities are Ed25519 keys, and none of it is a proprietary format), so keep that portability real by not scattering from agentmesh.governance import govern across two hundred call sites.

In a preview dependency, the import statement is the coupling. One seam you control is the difference between a version bump and a migration project.

What I'd tell someone starting this

Write the policy before you write the agent. If you cannot enumerate your tools and mark which are destructive, irreversible, or externally visible, you do not yet understand the system you are building. The policy file is a design artefact that happens to also be executable.

Governance is not compliance. A mapping table is a claim. Evidence is the deliverable. What survives an audit is decision records tied to a policy version, produced continuously, in a format someone else can read.

Isolate hardest where recovery is slowest. The same rule as any platform. A query you can rerun is cheap; an email you cannot unsend is not. Grade your action classes by reversibility and spend your approval friction there.

Assume the sequence, not the call. Per-action allow/deny is a necessary primitive and an insufficient model. Check separation of readers and senders before you ship.

Wrap it. Preview software sitting in the critical path of every tool call has earned exactly one interface you own.

The deeper point outlasts this particular repository. We spent a decade learning not to put authorization in the client, because the client is controlled by someone whose interests may not match ours. An LLM is a client whose interests are not adversarial, merely unreliable, and the remedy turns out to be the same one. Move the decision somewhere deterministic, log it, and stop asking nicely.


If you're putting agents into production under real regulatory pressure (telco, financial services, health), I'd genuinely like to compare notes on where you drew the enforcement boundary.

Share this post
Copied