Microsoft has published an “agent governance toolkit” that promises to make AI safer with identity, audit, and policy enforcement. The code does not deliver, however, because the authentication functions are disconnected. Let me explain where and how.

One Header, One Flag
This Microsoft toolkit is supposed to be a security checkpoint that sits between AI agents and the things they do. It has to verify the agent is who it claims to be before deciding what the agent is allowed to do, let alone logging what the agent did. But it doesn’t verify.
Welcome to what I noticed at commit 573f989.
The Go port of agentmesh exposes an HTTP middleware. The middleware reads an HTTP header into a struct field, and that struct field becomes the agent identity every downstream governance check trusts.
curl -H "X-Agent-ID: McLOVIN" <your-mcp-endpoint>
The audit log records the action under agent “McLOVIN,” while the rate-limit bucket counts against agent “McLOVIN” and the policy decision is attributed to agent “McLOVIN.” One header is one flag, and the entire chain of governance attaches to whatever string the caller chose to send.
The code lives in packages/agentmesh/middleware.go at line 241. The header flows directly into GovernedOperation.AgentID, with no authentication call anywhere on that path.
Five Ports, One Property
Across five languages, an agent’s claim about who it is becomes the system’s belief about who it is, with no verification step in between. Four of them accept the agent ID directly from caller input at the request entry point. All of them export a verify_peer-shaped trust primitive that production code never calls:
| Port | Entry-point ID source | Auth | Trust | Live callers |
|---|---|---|---|---|
| Rust | pub agent_id: String on DTO | None | TrustManager::verify_peer | 0 |
| Python | agent_id: str method parameter | None | MCPSessionAuthenticator, MCPMessageSigner | 0 |
| TypeScript | config.agentId at SDK construction | None (delegated to embedder) | TrustManager.verifyPeer | 0 |
| .NET | required string AgentId; integration falls back to literal “did:mcp:anonymous” | None on core entry points | TrustVerifier.VerifyPeer | 0 |
| Go | X-Agent-ID HTTP header | None | TrustManager.VerifyPeer (length-only) | 0 |
None of the ports bind a verified identity to the agent ID before it’s handed to the audit, policy, and rate-limit primitives.
Default Lie Enabled
The .NET port has the most important default. If the operator forgets to set up authentication, every action in the audit log gets attributed to the same fake identity, and the system never says a word.
The integration runtime tries three claim names from ClaimsPrincipal in order. If none match, it falls back to a caller-controlled Items dictionary, and if that is empty, it falls back to a hardcoded literal: “did:mcp:anonymous.”
Anonymous.
In any deployment that has not explicitly configured ASP.NET authentication middleware to populate one of the three claim names, every request gets attributed to that same anonymous identity. The audit log records one subject for everything. The rate limiter buckets all traffic against one key. The policy engine evaluates every action under the same subject, and so the deployment runs without any error or warning. There won’t be a log event acknowledging authentication isn’t really configured. It just sits wide open.
The operator watches the audit log fill, the rate limiter enforce, and policy decisions get recorded, yet the entire log resolves to one subject because the identity being attributed to every action is a literal string in McpGovernanceOptions.cs at line 68.
A correctly configured deployment and a misconfigured one therefore would look identical until someone spoofs a claim and the audit log records the wrong attribution.
Six Primitives, Zero Callers
It doesn’t matter that functions for security work exist when the live system never calls them. It’s like hiring security guards to wait for your phone call, in a room where they have no phone.
A workspace search for non-test callers of the security primitives in the Rust crate returns the same result for each: TrustManager::verify_peer, RingEnforcer::check_access, AuditLogger::verify, TrustManager::is_trusted, McpMessageSigner, McpSessionAuthenticator. Six security primitives, each exported, each tested in isolation, each with zero call sites in production code.
The pattern shows up in the other four ports. Five trust primitives across five languages, three of which replicate the verify-itself bug verbatim, with Go validating only key length and Python shipping separate session and message authenticators that no production caller invokes. The call-graph property is the same in every port: zero non-test callers.
No Locks, No Way
Microsoft built locks. Microsoft built doors. The doors have no way to install the locks. The MCP gateway constructor in Rust takes (config, sanitizer, rate_limiter, audit_sink, metrics, clock). The .NET constructor takes (config, sanitizer, rateLimiter, maxCallsPerMinute). The Go HTTP middleware constructor takes (rateLimiter, policyEngine, auditLog).
None of them takes an Authenticator, a Signer, or a TrustVerifier. The integration surface does not invoke the primitives, and there is no parameter on the constructors to plug one in. The primitives exist as exported symbols only, so the constructors that compose the request flow have no slot to receive them.
Verify-Peer Verifies Itself
The function meant to check whether you are who you say you are checks instead whether the local computer can sign things with its own key. Not great. It always can.
TrustManager::verify_peer in trust.rs at lines 188 to 193 takes a peer_id parameter. The parameter name has an underscore prefix, which the Rust compiler reads as “intentionally unused.”
The function generates a 32-byte random challenge, calls peer_identity.sign(&challenge), and verifies the resulting signature against peer_identity’s public key. All three steps happen in the local process. The local process signs a value of its own choosing using a key it already holds, then verifies its own signature. Any AgentIdentity passed in returns true.
The doc comment on the function reads: “Generates a random 32-byte challenge, asks the peer to sign it, then verifies the signature against the peer’s public key.”
The .NET TrustVerifier reproduces the same logic at Trust/TrustVerifier.cs lines 42 to 82, with a DID-equality precheck. The TypeScript port reproduces it at src/trust.ts lines 48 to 66. The Go port skips challenge-response entirely and validates len(PublicKey) == 32.
Audit Log
An audit log that does not survive a restart and breaks its own integrity check under normal load is not an audit log. It’s barely even a log.
The Rust AuditLogger holds entries in a Mutex<Vec<AuditEntry>> at audit.rs line 16, without any persistence layer. A process restart will wipe away every record from before the restart.
When with_max_entries(N) is set and the buffer overflows, entries.drain(..overflow) at audit.rs lines 62 to 69 removes entries from the front of the chain. After the drain, the new index-zero entry has a non-empty previous_hash field, while AuditLogger::verify at audit.rs lines 78 to 103 requires index-zero to have an empty previous_hash and returns false on that check.
The audit log silently transitions from “tamper-evident” to “always reports tampered” under a normal operating condition: buffer full. There is no checkpoint, no rotated-out signed root, no recovery path. AuditLogger::verify has zero non-test callers anyway, so the chain integrity check that breaks on eviction is never invoked from production code regardless. The audit subsystem ships an integrity primitive that nothing calls, that breaks under normal load, against an in-memory store that does not survive process restart.
Whack-a-crate
Fix the bug in one place and it stays unfixed in the other.
agent-governance-rust/agentmesh-mcp/ is byte-identical to agent-governance-rust/agentmesh/src/mcp/. Twelve files, every one matching under diff -q. The agentmesh-mcp directory ships to crates.io as agent-governance-mcp per its Cargo.toml, and no build script enforces sync between the two locations. Every finding in the mcp tree exists in two crates that ship to crates.io independently. A patch that fixes the agentmesh tree does not fix the agentmesh-mcp crate unless the patch is mirrored.
Different Surface, Same Class
Enclave’s writeup of CVE-2026-32173 documents an authentication failure in Microsoft’s Azure SRE Agent. The /agentHub WebSocket endpoint required a token to connect, and the underlying app registration was multi-tenant, which meant any Entra account from any company anywhere could obtain a valid one. The hub checked that the token was valid and that the audience was correct. It never checked that the caller belonged to the target’s tenant. Once connected, the hub broadcast every event to every client with no per-message identity filter.
A check that ran every step except the only one that mattered.
The agentmesh codebase shows the inverse: checks that would have mattered, written, exported, tested, and never invoked from any production path. Same class of bad architecture, two presentations.
The category here is AI agent governance, and the structural property is that the security surface and the integration surface drift apart unobserved. A review of the security surface finds primitives that look correct in isolation. A review of the integration surface finds caller-asserted identity strings flowing into audit, policy, and rate-limit consumers. Neither covers the gap between them.
What This Means For You
If you have this toolkit in production, check the audit log.
The agent ID it records comes from caller input. Nothing on the request path verifies that the caller is the agent it claims to be. An attacker spoofing the header gets attributed to whichever identity they chose to send.
Hand this to an auditor in the .NET case with default configuration and you are handing them a single-subject log of every action regardless of caller. The store behind that log is in-memory, does not survive a process restart, and breaks chain integrity on overflow. The trust primitives the marketing describes are in fact written, tested, and imported. No production code path calls them, so they may as well not exist.
This is the failure pattern to watch for in every AI agent governance product, not just this one.
Vendors are racing to ship “zero-trust identity” and “cryptographic verification” for agents because the OWASP Top 10 for Agentic Applications calls out Identity and Privilege Abuse (ASI03) and procurement asks vendors to demonstrate the mitigation. The primitives are the easy part. Wiring them into the request flow such that they actually run before audit, policy, and rate-limit consumers see the agent ID is the part that does not happen by accident.
What To Do
Audit any deployment of this toolkit for three things.
First, find every path where an agent ID enters the system from caller input (HTTP header, request body field, method parameter, SDK construction config) and confirm whether anything between that input and the governance consumers verifies the caller. In any deployment that follows the toolkit’s documented integration pattern, the answer will be no.
Second, in the .NET integration, do not rely on the default ClaimsPrincipal probe. Configure ASP.NET authentication middleware explicitly, populate one of the three claim names the toolkit reads, and treat any appearance of “did:mcp:anonymous” in your logs as a configuration alarm rather than expected output.
Third, do not treat the in-memory audit log as durable evidence. If you have a compliance obligation that touches agent activity, route audit entries to external persistent storage on write, not via post-hoc export. The chain integrity check is broken under normal operation, and nothing calls it anyway.
If you are evaluating any AI agent governance product, send an unsigned request to the running system and see if it’s rejected.
The test suite proves only isolation. A real test proves the primitive runs when a request arrives. A review focused on either surface alone misses this. I had to look across both to see it.
The Class Remains
The agentmesh toolkit ships authentication primitives in every language port with zero production callers in any of them. The verify_peer primitive in three ports signs values it chooses with keys it already holds and verifies its own signature. The audit subsystem is in-memory only, breaks chain integrity under normal load, and has zero callers of the integrity check. The same crate ships twice. Patches will close specific paths while the class remains open.
