Status: Draft v0.1 Last updated: 2026-06-04 Stability: Unstable , expect breaking changes before v1.0 —
This document specifies the Cedar policy bundle format, policy expression examples, enforcement modes, evaluation decision flow, and related governance features for the cMCP Runtime.
A policy bundle is a directory (or tarball) with the following structure:
bundle/
policies/ # Cedar policy files (.cedar), one per policy or logical group
schema.cedarschema # Cedar schema defining entity types, actions, and attributes
manifest.json # Provenance metadata
{
"version": "<semver>",
"authored_at": "<ISO8601>",
"author_identity": "<SPIFFE SVID or git identity>",
"commit_sha": "<git commit SHA>",
"approval_chain": [
{
"approver": "<identity>",
"approved_at": "<ISO8601>",
"signature": "<base64-encoded signature>"
}
]
}
approval_chain is optional. When present, each entry is a signed approval by an authorized reviewer. See Section 5 for how approvals are verified.
The bundle hash is the authoritative measurement committed into the attestation report. It is computed as:
SHA-256(canonical_json({
"manifest": <manifest.json contents>,
"policy_files": {
"<filename>": "<SHA-256 of file contents>"
// ... entries sorted lexicographically by filename
},
"schema_hash": "<SHA-256 of schema.cedarschema>"
}))
canonical_json means RFC 8785 (JSON Canonicalization Scheme): no insignificant whitespace, keys sorted lexicographically at every level. This ensures the hash is deterministic regardless of serialization order.
This hash is what gets measured into the attestation report (see policy_bundle.hash in the TRACE Claim). Any modification to any policy file, the schema, or the manifest changes the hash, producing a measurement mismatch that verifiers can detect.
The following examples show working Cedar policies for common enterprise use cases. All policies operate on the action Action::"call_tool".
Permit calls only to named tools:
permit(
principal,
action == Action::"call_tool",
resource
)
when {
resource.tool_name in ["salesforce.query", "snowflake.read"]
};
Explicitly forbid a specific tool regardless of other permits:
forbid(
principal,
action == Action::"call_tool",
resource
)
when {
resource.tool_name == "delete_customer_record"
};
Permit the call but instruct the response inspector to redact sensitive fields:
permit(
principal,
action == Action::"call_tool",
resource
)
when {
resource.tool_name == "crm.get_customer"
}
advice {
redact_fields: ["ssn", "payment_history"]
};
The advice block is evaluated by the response inspection pipeline after the upstream call returns. See response-inspection.md for redaction semantics.
Forbid tool calls to uncovered servers when the session carries HIPAA PHI sensitivity:
forbid(
principal,
action == Action::"call_tool",
resource
)
when {
context.session_sensitivity == "hipaa_phi" &&
resource.server_domain == "uncovered"
};
Permit tool calls only when the tool is in the workflow’s allowed set:
permit(
principal,
action == Action::"call_tool",
resource
)
when {
context.workflow_id == "customer_onboarding" &&
resource.tool_name in context.workflow_allowed_tools
};
Cedar is default-deny: a call is denied unless at least one permit matches and no forbid matches. To make this explicit and auditable, include a baseline forbid:
forbid(
principal,
action == Action::"call_tool",
resource
);
This ensures that even if the policy bundle is empty or all permits are removed, all calls are denied rather than silently allowed.
Enforcement mode is set in the deployment configuration, bound into the attestation report, and cannot change without an enclave restart. This makes the active mode tamper-evident.
| Mode | Cedar deny behavior | Audit entry |
|---|---|---|
enforcing |
Runtime rejects the call, returns a structured error to the agent | Logged with decision=deny |
advisory |
Runtime allows the call, forwards to upstream | Logged with decision=deny_advisory (would have been denied in enforcing mode) |
silent |
Runtime allows the call, forwards to upstream | Only a basic call log; no audit decision entry |
Structured error (enforcing mode):
{
"error": "tool_call_denied",
"tool_name": "<tool>",
"call_id": "<uuid>",
"policy_bundle_version": "<semver>",
"message": "Tool call denied by runtime policy."
}
The error does not include the matched rule name or policy text, to avoid leaking policy internals to the agent.
1. Receive MCP tool call request
inputs: tool_name, arguments, session_id, workflow_id (if present)
2. Build Cedar evaluation context:
{
tool_name,
server_identity,
server_domain,
session_sensitivity,
workflow_id,
workflow_allowed_tools,
user_identity // if available
}
3. Evaluate Cedar policies against:
(principal, Action::"call_tool", resource) with context
4. If decision = permit:
proceed to egress DLP check (see egress policy documentation)
5. If decision = deny:
enforce per enforcement_mode (see Section 3)
6. Log audit entry:
{ decision, rule_matched, latency_us, call_id }
7. If decision = permit and DLP passes:
forward call to upstream server
8. Receive response from upstream server
9. Run response inspection pipeline (see response-inspection.md)
applies advice blocks (e.g., redact_fields)
10. Log response audit entry
11. Return (possibly redacted) response to agent
Latency budget: Cedar evaluation target is under 1 ms for bundles up to 500 policy rules. The runtime measures and logs latency_us for each evaluation to support SLA monitoring.
The manifest.json provenance metadata is included in the bundle hash measurement (see Section 1). This means:
author_identity): tamper-evident. Changing the identity changes the bundle hash, producing a measurement mismatch that verifiers detect.authored_at): tamper-evident for the same reason.commit_sha): tamper-evident. Links the policy bundle to a specific point in version control history.approval_chain): tamper-evident. Each approval signature covers the bundle hash; removing or altering an approval changes the manifest, which changes the bundle hash.A verifier checking a TRACE Claim can perform the following steps:
policy_bundle.hash.policy_bundle.hash against the approved hash on record (e.g., from the organization’s policy registry).policy_bundle.hash.manifest.json to confirm author, timestamp, commit SHA, and approval chain.If any step fails, the verifier rejects the TRACE Claim. This process requires no trust in the operator: the TEE measurement is the root of trust.
Workflow identity is established via session metadata. The agent includes a workflow_id in the session initiation request:
X-MCP-Workflow-ID headerworkflow_id field in the session init payloadIf workflow_id is absent, the runtime defaults to workflow_id = "default". The default workflow policy should be restrictive (allowlist only widely-approved tools).
A tool call must pass both checks:
workflow_id per Cedar policy.Failing either check results in a deny decision.
The per-workflow allowed-tools list can be modeled as an entity attribute in the Cedar schema:
entity Workflow {
allowed_tools: Set<String>,
sensitivity_level: String
};
This allows Cedar policies to reference context.workflow_allowed_tools as derived from the Workflow entity loaded at evaluation time.
| Phase | Behavior |
|---|---|
| Phase 1 | Static workflow policies committed in the Cedar bundle. The workflow_id is trusted as declared by the agent. |
| Phase 2 | Dynamic workflow attestation: the agent cryptographically declares its current workflow; the runtime verifies the declaration before evaluating workflow-scoped policies. |
The agent’s MCP client is configured with exactly one MCP server URL: the runtime’s URL. All upstream servers are invisible to the agent; the runtime handles routing internally.
Example claude_desktop_config.json:
{
"mcpServers": {
"cmcp-gateway": {
"url": "http://localhost:8443/mcp",
"transport": "http"
}
}
}
The agent never learns the upstream server URLs. From the agent’s perspective, there is one MCP server. This prevents agents from bypassing the runtime by connecting directly to upstream servers.
To add an upstream MCP server to the runtime catalog:
catalog.json in version control (see tool-identity.md for schema).tool_catalog.hash).tool_catalog.hash in subsequent TRACE Claims.The new server is not reachable until the enclave restarts with the updated catalog. There is no runtime path to add a server without measurement.
If an unauthorized server must be accessed urgently without an enclave restart, the runtime supports a break-glass mode. Break-glass adds the server to a temporary exception list for the current enclave session.
Break-glass use is visible in the TRACE Claim:
"catalog_exceptions": [
{
"server_identity": "spiffe://corp.example/emergency/server",
"reason": "P0 incident -- customer data export required",
"authorized_by": "ops-lead@example.com",
"timestamp": "2026-06-01T03:17:00Z"
}
]
TRACE Claims with a non-empty catalog_exceptions list are flagged for auditor review. Break-glass use appears in all TRACE Claims for the duration of that enclave session.