All writing
ENTR
Let’s talk
WritingBuilding Secure Systems · 4 of 4

Policy, Not Hope

Microsoft’s Agent Governance Toolkit, read next to the contract I built by hand.

In the first three articles I kept circling the same sentence: behind every answer the model gives, there is either a verifiable source or an explicit refusal.

In the first article I laid out the problem: writing a chatbot and designing a safe system are not the same thing. In the second I turned the idea into a refusal and citation contract on the .NET side. No retrieval, no answer. If the model returns broken JSON, no answer. If the model refuses on its own, we don't override it. And if there's a citation, it must actually be in the list of sources that came from retrieval.

In the third I took it out of the mock world and tried it with real Azure AI Search, real Azure OpenAI and a real evaluation flow. So it's no longer at the "this is how I designed it" stage; we started measuring what the system actually does.

What we've really been doing across all three articles is this: putting a deterministic gate into the application code before the model's answer reaches the user. Instead of saying "please follow the rules", building a flow where an answer that breaks the rules simply can't get out.

I wrote that gate by hand, for RAG answers. Microsoft has published a more general version of the same idea as open source: the Agent Governance Toolkit.

Fig. 01 — Agent Governance Toolkit

A deterministic gate between intent and action

User request“Handle this for me”
Agent intentsearch_docs('refund policy')
Governance gateDeterministic check
  1. AllowThe call runs
  2. Require approvalA human signs off first
  3. DenyThe call never happens
# policy.yaml · illustrative
rules:  - name: block-destructive-db
    when: action.verb in [drop, delete, truncate]
    effect: deny  - name: external-email-needs-approval
    when: tool == send_email and recipient.external
    effect: require_approval  - name: role-gated-tools
    when: tool in supervisor_only and not agent.is(supervisor)
    effect: deny  - name: allow-read
    when: action.kind == read
    effect: allow

Every decision is logged

decision { agent: "agentassist", action: search_docs('refund policy'), rule: "allow-read", effect: allow }
  • effectIs this action allowed?
  • agentWhich agent did it?
  • decision logCan we prove what happened?
The rules come from the article; the YAML syntax is simplified for illustration. Pick an action.

Open the repository and you can tell it isn't a small demo; it's a serious attempt at a reference architecture (and Microsoft embracing this so firmly actually backs up our argument). It's in Public Preview and already at v4.1.0. The documentation lists 10 formal specs, 992 conformance tests and 29 ADRs. The first thing I noticed in Microsoft's repository was that I was looking at a more general, more disciplined version of the gate I had built by hand inside AnswerAssistantQueryHandler in my own repository.

In this article I'll read the toolkit not with "a new toy just came out" excitement, but as a continuation of the AgentAssist contract I built. Where we do the same thing, where the toolkit goes further, and where my handwritten solution still stays simpler: that's what this article is really about.

01A prompt is not a control layer

This was the core idea of the series. The Agent Governance Toolkit starts from the same problem: asking for safety at the prompt level isn't control; it's hope.

Writing "don't answer without sources", "don't use this tool" or "don't perform dangerous operations" into a system prompt may well be necessary. But in production, that alone isn't a security layer. The model works probabilistically, without certainty: sometimes it complies, sometimes it misunderstands, and sometimes it falls apart in the face of adversarial input.

This isn't just intuition. The ICLR 2025 study by Andriushchenko and colleagues reports, in their own experimental setups and benchmarks, very high attack success rates for adaptive jailbreak attacks against many models including GPT-4o, Claude 3/3.5 and Llama-3, reaching 100% in some scenarios. The key word is adaptive: the attack isn't a single "bad prompt", it's one that changes shape depending on the model and keeps probing until it finds the weak spot. So we can't say "I wrote it into the prompt, now I'm safe."

The toolkit's answer is clear: an agent's critical action must be caught at the application level before the model's intent gets out. A tool call? Check it. Sending a message? Check it. Handing work off to another agent? Check it. The result is either allow, deny, or require human approval. The difference looks small, but architecturally it changes everything:

Fig. 02 — Control

Prompt vs. policy

Prompt

says behave like this.

  • Probabilistic: sometimes it complies, sometimes it misreads
  • Adaptive jailbreaks: up to 100% success in some published setups
  • Nothing stops the action if it’s ignored
Policy

says if you don’t behave like this, you don’t get through.

  • Deterministic, evaluated before the action runs
  • Outcome: allow, deny or require approval
  • Every decision is logged
The difference looks small. Architecturally, it changes everything.

That's exactly what I did in the refusal/citation contract. I didn't trust the model's good intentions. If there was no source while answering, or the citation list couldn't be verified, I stopped the flow. The toolkit takes the same reflex beyond RAG answers and applies it to every action an agent takes. They sum it up in three questions:

  • Is this action allowed?
  • Which agent did it?
  • Can we prove afterwards what happened?

Those three questions are the backbone of a safe agent architecture.

02The toolkit at its simplest: enforcing an existing tool with policy

The toolkit's entry-level usage is deliberately simple. You mark an existing tool function with govern() and give it a policy file. From then on, every call is evaluated against the policy, the decision is logged, and if there's a violation the call stops.

You can start with YAML for policies. There's also OPA Rego and Cedar support, but YAML is enough for a first read. Simple examples:

  • Block destructive database actions like drop, delete and truncate.
  • Require human approval for sending email externally.
  • Prevent agents without a given role from calling certain tools.

This inevitably sent me back to my own code. What I did in AzureSearchFilterBuilder was a narrower version of the same logic. I didn't pass the role coming from the user straight into the query; I first ran it through an allow-list of verified roles. I also added an isActive eq true filter to every query.

The difference: I wrote that in C# with if statements and builder logic; the toolkit moves the same idea into a policy file.

That distinction matters. When a rule is buried in code, you need to know the application code to read it. When the rule moves into a policy file, it can be versioned separately, linted, checked in CI, and shown to an auditor as "this is our rule set".

Is that necessary for every system? No. But once an agent starts calling tools, especially if it can write to external systems, the separation stops being a luxury and becomes a need.

03The toolkit's layers: you don't have to take them all

What I like about the toolkit is that it doesn't behave like a monolithic framework that's hard to swallow (Microsoft started taking this approach about ten years ago with .NET Core). It's designed in layers, and you can start from whichever one you need.

Fig. 03 — Architecture

The toolkit’s layers: take only what you need

  1. L6Agent HypervisorExecution control, audit and commitment anchoring. The more advanced controls.
  2. L5Agent ComplianceOWASP verification, policy linting and evidence generation.
  3. L4Agent SREKill switch, SLOs, chaos, circuit breakers: production reliability.
  4. L3Agent RuntimeExecution boundaries with privilege rings: what an agent may touch.
  5. L2Agent MeshDiscovery, routing, identity and trust between agents.
  6. L1Agent OSPolicy engine, agent lifecycle and the governance gate. The core.core · start here

Also worth a look

  • MCP Security Gatewaytool poisoning, drift, typosquatting, hidden instructions
  • PromptDefense Evaluator12-vector prompt-injection assessment
  • Shadow AI Discoveryfinds unregistered agents inside the org
  • Governance Dashboarddecisions, violations, agent health

For AgentAssist today, policy + audit alone is already valuable. Taking every layer at once would be a mistake.

For my current AgentAssist scenario I don't need all of these; taking them all at once would actually be a mistake. But the policy and audit layer is valuable even on its own.

I think that's good design. In real systems, security layers that come as "all or nothing" often don't get adopted. Here there's at least a way to start small and grow.

04Saying "safe" isn't enough; you need to produce evidence

One of the sentences I cared about most in this series was: a contract you can't prove isn't a contract. What drew me most on the toolkit side was the CLI and evidence generation. The standout commands:

Shell
agt verify
agt verify --evidence ./agt-evidence.json --strict
agt red-team scan ./prompts/ --min-grade B
agt lint-policy policies/

Their value is this: you don't just write your security claims into a document, you make them measurable in CI. For example, with agt verify --evidence --strict you get to the idea of producing evidence for OWASP Agentic Top 10 coverage and failing the build if something's missing.

That's very close to my two-layer evaluation approach. In Layer 1, I measured contract behavior: does a refusal come back when there's no source, does an unauthorized document ever reach the model, does the system stop a model that returns a made-up citation? The toolkit extends that into a broader agent governance scope.

That's also why the mapping to standards matters: they describe mappings and evidence generation for frameworks such as OWASP Agentic Top 10, NIST AI RMF, the EU AI Act and SOC 2. We need to be careful here: this doesn't mean "I used this system, so I'm automatically compliant". But it strengthens the material you bring to an audit conversation.

Anyone who works in a regulated field knows that "we made it safe" means nothing on its own. An auditor wants to know:

  • Which rule was active?
  • Which action was requested?
  • On what basis was it allowed or denied?
  • Was the decision stored so that it can't be altered afterwards?

The toolkit's audit and compliance side is trying to produce answers to those questions.

05The .NET side: it's there, but set your expectations right

As someone who works in .NET, I want to be especially honest here. The toolkit is multi-language: Python, TypeScript, .NET, Rust and Go. The README says all five support the core governance features: policy, identity, trust and audit. On the .NET side there's the Microsoft.AgentGovernance package, plus Microsoft.AgentGovernance.Extensions.ModelContextProtocol for MCP.

But the full stack is still on the Python side. For runtime sandboxing, SRE and some of the more advanced layers, the Python ecosystem is fuller.

Fig. 04 — .NET

.NET today: set the expectation right

Core governance in all five: policy, identity, trust, auditPythonTypeScript.NETRustGo
Capability.NETPython
Policy evaluationAvailableAvailable
Agent / tool-call governanceAvailableAvailable
MCP integrationAvailableAvailable
Audit and the base trust layerAvailableAvailable
Runtime sandboxingFuller in PythonAvailable
Agent SREFuller in PythonAvailable
More advanced layersFuller in PythonAvailable
.NET packagesMicrosoft.AgentGovernanceMicrosoft.AgentGovernance.Extensions.ModelContextProtocol
As of the Public Preview described in the article.

That isn't a bad thing; it just needs to be written down up front when you make an architectural decision. If I were adding this to a .NET architecture like AgentAssist, I'd put this note in the ADR:

The Agent Governance Toolkit is in Public Preview. On .NET, core governance and MCP integration are usable; full-stack capabilities are broader on the Python side. Initial use will therefore be limited to CI/evidence and a policy gate.

That sentence matters, because even when the technology is right, its maturity level is part of the architectural decision.

06Side by side with AgentAssist

This is the real reason for this article. The more I read the toolkit, the more I realized that in AgentAssist I had built a narrow, handwritten version of the same approach. Same philosophy, different scope.

My system is a single-flow RAG/agent-assist scenario. No tool calling, no multiple agents, no writing to external systems. That's why the handwritten contract is still very readable and very controlled. Side by side, the picture looks like this:

Fig. 05 — Mapping

AgentAssist → Agent Governance Toolkit

The toolkit-side counterpart of the handwritten contract

Not a request, a policy: the model’s answer or the agent’s action passes through a deterministic gate.

  1. 01Refusal rules inside the orchestratorYAML policy + policy engineThe rule leaves the code: it can be linted and checked in CI
  2. 02Role allow-list + isActive filterPolicy conditionsAuthorization and visibility rules become central
  3. 03Citation whitelist checkA deny decisionA contract violation is logged as a governance decision
  4. 04Azure SQL audit + App InsightsTamper-evident audit + Decision BOMStronger audit and provenance
  5. 05A single adversarial prompt-injection testPromptDefense + red-team scanInjection measurement becomes systematic
  6. 06Contract eval + quality evalagt verify --evidence --strictGovernance coverage is tied to evidence in CI

Same discipline, different level. AgentAssist builds the contract into a single RAG flow; the toolkit generalizes it to agent actions.

Building this table left me with two feelings. First, I had landed in the right place, because the toolkit doesn't reject the approach I built; it generalizes it. Second, the limits of my solution became more visible too. Handwritten if statements are very clear in a single flow, but as the number of agents, tools and external systems grows, the same method eventually falls apart.

07Where mine is narrower, but more controlled

Saying "let's switch to the toolkit right away" doesn't feel right to me here.

Today AgentAssist is a single-flow system. It does retrieval, applies a permission filter, calls the model, validates citations and returns a refusal when needed. That's it. At this level of simplicity, a handwritten contract has a serious advantage: anyone can read the code and understand the flow. You can see directly when a refusal is returned and when a citation is considered invalid.

A general framework doesn't always bring an advantage at this level of simplicity; sometimes it just adds a layer of abstraction. In architectural terms, that has a very simple name: over-engineering.

But if the system grows, the picture changes. If the agent starts calling tools, for example:

  • runs SQL queries,
  • sends email externally,
  • creates records in a CRM,
  • hands work off to another agent,
  • discovers new tools through MCP servers,

then keeping track of handwritten checks gets hard. Because the question is no longer only "is the answer right?"; it's also "is this action allowed?". That's where the toolkit really shines.

For a RAG answer, a contract may be enough. For an agent's action, you need policy.

08How I would integrate it

If I were applying this to AgentAssist today, I wouldn't draw up a big migration plan. I'd start small.

Fig. 06 — Plan

Start small: a three-step plan

  1. 01Now
    Add evidence to CI

    Lowest risk. The application flow isn’t touched.

    agt verify --evidence ./agt-evidence.json --strict
  2. 02Next
    Model the contract as policy

    A step of the orchestrator in the Application layer. The Domain keeps the meaning; the engine enforces it.

    • deny if retrieval returns nothing
    • deny if a citation ID isn’t in the retrieved set
    • require_approval if the query is high-risk
    • deny if a tool action is destructive
  3. 03If needed
    Expand when MCP and tool calling arrive

    MCP Security Gateway and agent identity start to matter once the agent can act.

    “With this many tools and agents, how do we trust any of it without policy?”

Bars = how much of the running system each step touches

Step one: add evidence to CI. This is the lowest-risk step. Try agt verify, and if possible agt verify --evidence --strict, without touching the application flow. The aim isn't to say "I've put the toolkit into the production flow", but to set the existing security story next to a framework like the OWASP Agentic Top 10. It also fits the evaluation logic from the third article: measure first, think about enforcement later.

Step two: model the refusal/citation contract as policy. Next, you could try moving some rules into a YAML policy. For example:

  • deny if retrieval returns nothing,
  • deny if a citation ID isn't in the retrieved set,
  • require_approval or escalation if the query is high-risk,
  • deny if a tool action is destructive.

In a Clean Architecture setup I'd place this as a step of the orchestrator in the Application layer. The Domain still carries the meaning of the contract; the policy engine becomes the enforcement mechanism.

Step three: expand if MCP and tool calling arrive. It's too early today. But if AgentAssist later moves towards MCP servers, tool calling or multi-agent scenarios, the MCP Security Gateway and the agent identity layer become genuinely meaningful. At that point the question won't be "should we use the toolkit?". It will be: with this many tools and agents, how do we trust any of it without policy?

Closing

At the start of this series I said "design systems, not chatbots". Four articles later, what I keep coming back to is this: the key to building a safe AI system isn't choosing the model, it's designing the control points. The model produces the answer, but the system makes the decision. The system determines where it speaks, where it stays silent, which actions it may take, and how it proves all of that.

That's why the Microsoft Agent Governance Toolkit caught my attention. What it describes arrives at the same place as the line I've been defending for three articles:

Fig. 07 — Summary

The line, in four sentences

  1. 01Don’t write it in the prompt.
  2. 02Set a policy.
  3. 03Log the decision.
  4. 04Produce evidence.

Would I move my handwritten contract entirely onto this toolkit tomorrow? Not today. In a single-flow RAG scenario, the existing contract is simpler and more readable. But if the system grows towards tool calling, MCP or a multi-agent structure, the answer changes. At that point handwritten gates aren't enough; central policy, agent identity and an audit layer become an architectural necessity.

What matters isn't the tool. What matters is having set the rules.

Sources

This English version is a translation of the original Turkish article.