An agent calls a tool. The policy engine says allow. The call goes through.
None of that proves the policy engine was not compromised. Software-only governance of MCP tool calls cannot guarantee three things, and they are worth naming separately because they fail in different ways.
The Cedar policy on disk may not be the policy that ran. A rogue admin can swap the bundle after approval, and the hash check that would catch it runs inside the same operating system that admin controls. The allow decision may have been flipped in memory, because a supply-chain CVE in the evaluator executes in the same address space as everything else. And the audit log may not reflect what happened, because any party holding the software signing key can reconstruct a valid-looking chain afterwards.
Those are not exotic. They are the ordinary consequences of running the thing that decides in the same place as the thing it decides about.
Where cMCP puts the decision
cMCP is a gateway in front of your MCP servers, and its one structural commitment is that the policy engine runs inside a hardware Trusted Execution Environment, where the agent being governed cannot reach it. Every tool call is intercepted and evaluated against a Cedar policy bundle before it goes anywhere.
The mechanism that makes that more than an assertion: before the gateway serves a single tool call, it measures its installed code, its policy bundle and its config, and binds that digest into the hardware-signed attestation report. You are not being asked to believe the gateway loaded the approved bundle. On SEV-SNP and TDX the digest sits in the report’s report_data, on Azure confidential VMs it rides in a vTPM quote that the SNP report vouches for, and on the TPM tier it is extended into a certified NV index. Either way a verifier learns which code and which policy were in force from the same evidence that establishes the enclave, and when the bundle reloads mid-session the gateway re-attests rather than leaving the old report standing.
The payloads go inside too. In a hardware deployment the tool-call payload is processed inside the enclave, so the policy decision and any redaction happen where the host cannot reach them. What the host and the connectivity provider can still read depends on the egress policy, and the upstream tool server is its own component outside the TEE. The attestation covers the gateway boundary, not what happens on the far side of it.
Deny is not the only verdict, which matters more than it sounds. A policy can redact a response rather than block the call, and the catalog carries a per-tool schema_validation_mode that selects which. A hard block on a tool an agent genuinely needs does not end in compliance, it ends in somebody routing around the gateway.
The deliverable is a signed TRACE claim, per session or per call, and a verifier checks it without trusting the operator who produced it. Run the gateway in a TEE and the claim is hardware-attested; run it in software mode and it is signed only, and the claim states which of the two it is rather than leaving a reader to assume the stronger one.
Software mode is also how you should meet it. pip install cmcp-runtime needs no hardware and gives you the whole shape of the thing before you decide whether to provision for attestation.
Version 0.5.0 shipped on 6 September. One change in it is a plain correctness win: the policy bundle hash now uses RFC 8785 canonical JSON, which is what docs/spec/cedar-policy.md section 1 always specified. Until then the implementation and its own specification disagreed on how to hash the object the entire enforcement chain anchors to.
The record it writes about itself
Here is a thing cMCP records that most gateways do not. When a session has read sensitive data and then calls a tool that leaves that sensitivity domain, the boundary crossing goes into the claim as an event. That is the difference between knowing an agent read patient records, and knowing it read patient records and then called an external API.
The check is a set membership test, and the gateway answers it from a set it holds itself. Which is where a control plane writing its own record gets interesting, because nothing outside it is checking that the set is the right one.
In 0.4.1 the set was written out by hand in session/call_log.py as {“pii”, “phi”, “pci”, “restricted”}. The vocabulary the rest of the runtime uses lives one file away in session/state.py, ranking public 0, pii 1, confidential 2, and hipaa_phi, mnpi and trade_secret all at 3.
Read those together. They intersect on pii and nothing else. phi, pci and restricted are not names the runtime uses anywhere, and all three top-tier domains were absent. So the check fired on one domain, the lowest tier above public, and a session that read HIPAA PHI then called an external tool recorded no crossing. The claim was signed, the attestation was valid, the verifier was satisfied, and the event was not in it.
0.5.0 derives the set from one COMPLIANCE_DOMAINS vocabulary, keeping the legacy spellings so no deployment loses events.
The codebase had already written this failure mode down. A docstring in state.py warns that a name dropping out of the effective vocabulary will rank 0 through a fail-open get(tag, 0) default, and names it the same class of hole that catalog-load validation was added to close. That comment sits one file away from a hand-written set holding three names the vocabulary did not have. The defence was attached to a mechanism rather than to a rule, so nothing noticed the same shape next door.
What to verify
The general form is a question you can put to any governance component, ours first: when this thing records a fact about its own behaviour, where does the vocabulary for that fact come from, and what happens when the value it sees is not in it?
Find every place your control plane holds a literal set of category names, find where those categories are actually defined, and if the two are not derived from one source, compute the intersection by hand and look at what falls outside.
Then ask what the failure does. A membership test returning False on an unrecognised value fails open, quietly, and produces a record that is correct in every field it contains. That is worse than a crash, because a signed and complete-looking claim invites more trust than an error does.
Last, check whether anything goes red when the vocabularies diverge. Not whether today’s values line up. Whether a test fails when somebody adds a seventh domain to one file and not the other.
What does your evidence say when the thing it describes uses a word your recorder has never heard?
Top comments (0)