Blueprint · part 4 of 6
Action governance with Microsoft's Agent Governance Toolkit: how the two empty rows get an answer
Part 3 left two demands without a box in the stack: a person who can override or stop the agent, with the authority to (AI Act article 14), and a way to keep the agent within its intended use for the task at hand (ISO 42001 A.9.4). Microsoft's Agent Governance Toolkit, MIT-licensed and public since March 2026, is the most complete open answer I have found to both. Here is what it does, read in its code, and where it plugs into the stack of parts 1 and 2.
HokonokenSeptember 2026Reading time: 11 minViews are my own, not my employer's
Three things to take away
- It governs the action, not the text. Guardrails filter prompts and answers. The toolkit decides on each tool call: this action, with these arguments, for this agent, allowed, denied or escalated to a human. That is the grain the regulations talk about.
- Intent is a real object in the code. An agent declares what it plans to do; the plan is approved; anything outside it is blocked or forces a re-declaration; a sub-agent's intent can only be narrower than its parent's. That is "intended use" enforced per task, not per role.
- It is a library and a sidecar, not a platform. No console, no Kubernetes admission, partial tenant isolation. It plugs into the stack at three points; the stack around it stays what it was in parts 1 and 2.
What it is
The Agent Governance Toolkit is a monorepo published by Microsoft on 2 March 2026 and announced on 2 April 2026, at version 5.0.0 as I write. Seven packages, five languages (Python, TypeScript, Rust, .NET, Go), and a specification called Agent Control Specification whose reference runtime lives in a separate Rust crate. The code I read is the public repository at commit e7f5d2b of 19 September 2026. Everything below is a reading of that code; none of it has been executed.
The four capabilities, and the row each one answers
Policy per action. Rules are written in Cedar or Rego and shipped as bundles: twelve Rego modules and eleven Cedar files in the repository. The grain is the action type plus its arguments, so a rule can say "outbound HTTP to this host is denied" or "writes to this table escalate". Verdicts are allow, deny or escalate. This is NIS2's "access control policies" at the grain that matters for an agent: the tool call, not the login.
Sector and compliance templates. The repository also ships content, not only mechanism: four sector starters (financial services, healthcare, K-12 education, general SaaS), each an Agent Control Specification manifest bound to a Rego bundle of about 220 lines whose rules carry OWASP Agentic Top 10 identifiers, and fifteen "kernel" policy templates in the agent-os package (GDPR, HIPAA, PCI-DSS, SOX, data protection, multi-tenant, rate limiting, secure coding, environment profiles) written as pattern-based deny rules that raise a pause or a kill signal. All are marked as starting points to customise. The docs folder adds an EU AI Act checklist, an ISO 42001 mapping and a CIS Controls mapping. For the rows of part 3, this means the sector vocabulary is already there; what is missing is the local regime, FINMA or the Swiss FADP for instance, which nobody ships.
Declared intent. In agent_os/intent.py, an ExecutionIntent carries a list of planned actions and moves through declared, approved, executing, then completed, violated or expired. A drift policy decides what happens to an action outside the plan: soft block, hard block or re-declare. A child intent, created for a sub-agent, must be a subset of its parent's planned actions, and the code raises an error otherwise. This is the closest thing I have seen in open code to ISO 42001's "intended use of the AI system" enforced at runtime, and to the FADP's "purpose of processing" applied to an agent rather than a database.
Human approval. The escalation verdict can be resolved by a webhook, by MCP elicitation when the client supports it, or by notifications to Slack, Teams or PagerDuty, all present as production code. The webhook is designed to fail closed: on timeout, on transport error, or if the answer does not echo the request id and the digest of the action, the decision is deny. The approver is recorded. This is article 14's override and stop, and article 26(2)'s "necessary competence, training and authority", with the who-approved-what written down.
Tamper-evident audit. agentmesh/governance/audit.py keeps a Merkle tree over the audit entries, with a root hash and inclusion proofs; a second component, the flight recorder, chains entries by SHA-256. An auditor can verify that an entry was there and has not been altered since. Article 12 asks for logs; FINMA asks that results can be "understood, explained or reproduced". A verdict per action, with the rule that produced it, is the explainable part; the tree is the reproducible part.
Where it plugs into the stack
In the agent pod, as a library inside the harness, for which there are SDKs in all five languages, or as a network sidecar next to it: the repository ships a FastAPI sidecar on port 8001 and a Go one. This is where intent has to live, because only the runtime knows what the agent is about to do. It works for both patterns of part 1: the agent in its OpenShell pod, and the shared OGX server.
At the MCP gateway, as the external guardrail. Both gateways of parts 1 and 2 already know how to hand a tool call to an external service before executing it. Red Hat's MCP gateway posts the tool name and arguments to a guardrail endpoint and applies allow, block or modify, failing closed if the service is down; Envoy AI Gateway does the same through an ext_proc on the MCPRoute. That contract is not the toolkit's, so an adapter is needed, but the seam exists. This is the one place in the stack where a tool call can be held while a human decides.
Behind the ingress authorization. Authorino accepts an external OPA policy and HTTP calls; Open Policy Agent evaluates Rego natively. The toolkit publishes its rules as Rego and exposes a decision service. One bundle can therefore be evaluated at the door, at the grain of server and tool, while the arguments are checked at insertion point 2.
One tool call, end to end
Two details in that sequence carry the regulatory weight. The approval answer must echo the request id and the digest of the exact action, or it is treated as a denial: a human cannot approve something other than what will run. And the audit entry is written whether the verdict was allow, deny or escalate, so the absence of an entry is itself evidence.
What the configuration looks like, in the toolkit's own files
Five excerpts, all taken from the public repository at commit 2e98345 of 21 September 2026, with the file path above each. They show where the four capabilities of this article are switched on: which tool calls are intercepted and at what grain, what happens on an "escalate" verdict, what a sample governance policy looks like, what an intent contains when it is declared, and what the sector and compliance templates already contain.
1. The policy manifest: where a tool call is intercepted
policy-engine/examples/records_agent/manifest.yamlYAML
# Agent Control Specification manifest (excerpt): the pre_tool_call intervention point
# hands the tool name and its arguments to a Rego query before the call runs
agent_control_specification_version: 0.4.0-alpha.1
metadata:
name: medical_records_assistant_guardrails
policies:
medical_records_assistant_guardrails:
type: rego
bundle: ./policy
query: data.agent_control_specification.medical_records_assistant_guardrails.verdict
intervention_points:
pre_tool_call:
policy_target: $.tool_call.args
policy_target_kind: tool_args
annotations:
access_scope:
from: $.tool_call.args
policy:
id: medical_records_assistant_guardrails
query: data.agent_control_specification.medical_records_assistant_guardrails.pre_tool_call_verdict
tool_name_from: $.tool_call.name
tools:
fetch_record:
type: Tool
id: fetch_record
clearance: [medical_records]
security_labels: [patient_record]
export_data:
type: Tool
id: export_data
clearance: [medical_records, data_export]
security_labels: [bulk_phi]
excerpt; the same file declares input, pre_model_call, post_model_call, post_tool_call and output points
2. The approval section: what happens on "escalate"
policy-engine/core/schema/approval.schema.jsonYAML · shape from the schema
# Shape of the optional top-level "approval" section of a manifest, as defined by
# approval.schema.json. No example manifest in the repository uses it yet: the engine
# validates the shape and treats the resolver configuration as opaque host config.
approval:
default_resolver: reviewers # absent → the escalate verdict resolves to deny
timeout_seconds: 300 # must be > 0
on_timeout: deny # deny | allow | suspend
fatigue_threshold: 20 # soft cap on approvals per agent per window
fatigue_window_seconds: 3600
resolvers:
reviewers:
type: webhook # discriminating field; the rest is host-defined
url: https://approvals.example.internal/agt
Field names, enum and constraints from the schema. The webhook itself is agentmesh/governance/approval_webhook.py, which denies on timeout, transport error, or an answer that does not echo the request id and the action digest.
3. A sample governance policy: approval, delegation, audit
examples/policies/adk-agt-manifest.yamlYAML
# Sample policy shipped for Google ADK agents; the repository marks it as a starting
# point to customise, not a production policy
version: "1.0"
name: adk-governance
adk_governance:
blocked_tools:
- execute_shell
- run_command
- delete_database
- drop_table
max_tool_calls: 100
require_approval_for: # these go to a human before execution
- send_email
- publish_document
- deploy_service
- transfer_funds
delegation:
max_depth: 3
require_scope_narrowing: true # a sub-agent may only get a narrower scope
audit:
log_all_tool_calls: true
log_delegations: true
include_tool_args: false # "Set true only in dev (may contain PII)"
comments shortened
4. The intent: what an agent declares before it acts
agent-governance-python/agent-os/src/agent_os/intent.pyYAML · rendering of ExecutionIntent.to_dict
# An intent is declared from code (IntentManager.declare_intent) and serialised by the
# toolkit as a dictionary; this is that dictionary rendered as YAML, with the values of
# the repository's intent-auth demo
agent_id: bank-agent
planned_actions:
- action: read_balance
- action: transfer_funds
params_schema:
max_amount: 1000 # an argument outside this is off-plan
drift_policy: hard_block # soft_block | hard_block | re_declare
parent_intent_id: null # set for a sub-agent: its actions must be a subset
expires_at: "2026-09-22T15:00:00+00:00"
Classes ExecutionIntent, IntentAction and DriftPolicy; values from examples/intent-auth/intent_auth_demo.py. Not a file format the toolkit reads: it is what the toolkit writes, and what the audit entry carries.
5. The sector and compliance templates the repository ships
examples/policy-templates/financial-services.yamlYAML · sector starter
# Sector starter shipped in the repository: a manifest that binds the
# financial-services Rego bundle to the input and output intervention points
agent_control_specification_version: 0.4.0-alpha.1
metadata:
name: financial-services-asi-starter
version: "1.0"
policies:
financial_services:
type: rego
bundle: rego
query: data.agt.examples.templates.financial_services.result
intervention_points:
input:
policy_target: $.input.body
policy:
id: financial_services
output:
policy_target: $.output.content
policy:
id: financial_services
examples/policy-templates/rego/financial-services.regoRego · excerpt
# Two of the rules in the bundle (227 lines), organised by OWASP Agentic Top 10 identifiers
candidates contains {"name": "asi02-block-network-exfiltration", "action": "deny", "priority": 100,
"message": "ASI-02: Outbound data transfer tools are prohibited without explicit allowlisting"} if {
regex.match(`^(http_post|http_put|upload_file|send_data|ftp_upload)$`, sprintf("%v", [object.get(context, "action", 0)]))
}
candidates contains {"name": "asi03-account-mfa-bypass", "action": "deny", "priority": 100,
"message": "ASI-03: Account Integrity — unauthorized attempt to disable MFA or authentication lockouts"} if {
regex.match(`(?i)(remove|disable|bypass|waive)\s+(mfa|2fa|multi-factor|lockout|authentication)`, sprintf("%v", [object.get(context, "output", 0)]))
}
agent-governance-python/agent-os/templates/policies/gdpr.yamlYAML · kernel template, excerpt
# One of fifteen "kernel" policy templates in agent-os: gdpr, hipaa, pci-dss, sox-compliance,
# data-protection, multi-tenant, api-gateway, content-safety, cost-controls, rate-limiting,
# secure-coding, production, development, research, enterprise
kernel:
version: "1.0"
mode: strict
template: gdpr
signals:
enabled:
- SIGSTOP # Pause for DPO review
- SIGKILL # Terminate on violation
- SIGCONT # Resume after approval
policies:
- name: pii_contact_detection
description: Block exposure of contact information
severity: critical
category: compliance
scope: [input, output]
deny:
- patterns:
- '(?i)(email|e-mail)\s*[:=#]\s*["'']?[\w.+-]+@[\w-]+\.[\w.-]+["'']?'
action: SIGKILL
message: "GDPR: Contact PII detected — data must be pseudonymized or removed"
Two families. The four sector starters (financial services, healthcare, K-12 education, general SaaS) are Agent Control Specification manifests bound to Rego bundles of about 220 lines each, whose rules carry OWASP Agentic Top 10 identifiers (ASI-01 prompt injection, ASI-02 tool misuse, ASI-03 identity abuse, and so on), with test fixtures. The fifteen kernel templates in agent-os (GDPR, HIPAA, PCI-DSS, SOX, data protection, multi-tenant, rate limiting, secure coding, and environment profiles) are pattern-based deny rules with a signal to raise: SIGSTOP pauses for a human, SIGKILL terminates. Both families are marked as starting points to customise, not production policies, and I found no loader for the kernel templates in the core package: wiring them in is the operator's job. The docs folder adds mappings rather than code: an EU AI Act checklist, an ISO 42001 mapping, a CIS Controls v8.1 mapping and an MCP OWASP Top 10 mapping.
Three things these files make visible. The sector templates are content, not mechanism: the same engine, a different Rego bundle per sector, which is how a regulated deployment starts. The audit is on by default and the arguments are off by default: include_tool_args: false is the repository's own advice, because arguments carry personal data. And the intent is not something you configure; it is something the agent declares at runtime, which is why it ends up in the audit entry and not in a YAML file. Part 5 follows those entries out of the process.
What it does not do
- No Kubernetes admission. Nothing in the repository validates or mutates workloads at admission; the toolkit governs an agent that is already running. Kyverno or Gatekeeper keep that job.
- No console. The human surfaces are IDE and browser extensions, plus the notification channels. There is a dashboard API in the code, with no frontend behind it.
- Tenant isolation is partial. Policies can be scoped per organisation, but the services themselves are documented for single-tenant use.
- Intent lives in one module. It is in the agent-os package, not in the policy-engine core or in the language SDKs; if you use only the SDK, you get policy and audit, not intent.
- The audit tree has no external witness. The tree proves that the operator did not alter the log. It does not prove it to a third party; the code says explicitly that it does not anchor to a transparency log. Part 5 comes back to this.
Net effect on the matrix of part 3. The human-oversight row gets a strong cell at the MCP gateway, and the intended-use row gets a strong cell in the agent runtime. Both are answered by open code, deployable on-premises, with no account at any vendor. What remains open is proof to an outsider, and that is the subject of part 5.
Next in the series
- Part 5Observability: how it is proven. What OpenTelemetry, OCSF and MLflow record, what they cannot tell you, and why an audit tree needs a witness.
- Part 6Rolling out a model blue/green: how the stack changes without breaking what parts 3 to 5 established.
Read, not run. Everything in this series comes from reading public code and documents at a stated date, not from running them in production. Treat it as a map to test, not a result to trust: these projects move monthly, their bugs move with them, and a component marked preview or alpha here may be stable, or gone, by the time you read this. Test it on your own cluster. When something does not match, file the issue in the project's tracker and send the fix back: that is how open code improves, and it is the only way a map like this one stays true.
Sources
- microsoft/agent-governance-toolkit, MIT, read at commit e7f5d2b (19 September 2026); files cited:
agent-governance-python/agent-os/src/agent_os/intent.py, agent-governance-python/agent-mesh/src/agentmesh/governance/approval_webhook.py, agent-governance-python/agent-mesh/src/agentmesh/governance/audit.py, policy-engine/policy/lib, policy-engine/policy/cedar-lib, examples/policy-templates/, agent-governance-python/agent-os/templates/policies/, docs/compliance/
- Same repository at commit 2e98345 (21 September 2026), for the configuration excerpts:
policy-engine/examples/records_agent/manifest.yaml, policy-engine/core/schema/approval.schema.json, examples/policies/adk-agt-manifest.yaml, agent-governance-python/agent-os/src/agent_os/intent.py, examples/policy-templates/financial-services.yaml and rego/financial-services.rego, agent-governance-python/agent-os/templates/policies/gdpr.yaml
- Introducing the Agent Governance Toolkit, Microsoft Open Source Blog, 2 April 2026
- Agent Governance Toolkit: architecture deep dive, Microsoft Community Hub
- Kuadrant/mcp-gateway, guardrail contract in
internal/guardrails/checker.go; Envoy AI Gateway, MCP
- Kuadrant/authorino, AuthConfig CRD:
opa.externalPolicy, http
Independent work, not affiliated with Microsoft, Red Hat, NVIDIA or the CNCF. Product names belong to their owners. Views are my own and do not represent the position of my employer. Text and diagrams: CC BY 4.0; quoted code and documents stay under their own licences.