Policy authorization — Cedar, OPA, Casbin
Authorize tool calls, prompts, and resources. Three policy engines plus a built-in trust floor; chain them for defense-in-depth.
After identity, MCPG asks: can this caller do this thing? Two mechanisms answer:
- The built-in trust floor + CEL gate under
governance.policy.tool_access— a per-tool minimum-trust level plus optional CEL predicates. No plugin required. - Policy engines chained under
governance.policy.engine[]— Cedar, OPA, or Casbin, each loaded as aclass: policy_engineplugin.
The built-in trust floor
Every tool call is gated by a minimum trust level and optional CEL allow_if rules before
any engine runs. This alone covers a lot of ground:
governance:
policy:
tool_access:
default_minimum_trust: verified
rules:
- tool_name: "admin.*"
minimum_trust: verified
cel_allow_if: '"platform" in identity.groups'
- tool_name: "directory.users.list"
minimum_trust: verified
cel_allow_if: 'identity.attributes.tenant == "acme"'
A deny here is observable through the standard mcpg.policy.tool_call.denied audit shape.
Per-binding overrides live on the binding's own governance: block (see below).
The policy engines
| Engine | Best for | Languages |
|---|---|---|
cedar (dev.mcpg.policy.cedar) | Sub-millisecond evaluation, structured conditions | Cedar (AWS) |
opa (dev.mcpg.policy.opa) | Existing Rego policies, complex data lookups | Rego |
casbin (dev.mcpg.policy.casbin) | RBAC / ABAC with explainable denies | Casbin DSLs |
Engines are listed in governance.policy.engine[] as a chain. The host walks the list in
order, short-circuiting on the first Allow / Deny; NotApplicable falls through to the
next engine. Each plugin must also be loaded under plugins[].
Cedar (recommended for new deployments)
Fast, in-process, hot-reloadable. Sub-millisecond per evaluation:
// policies/admin.cedar
permit (
principal in Group::"platform",
action == Action::"github.list_repos",
resource == Tool::"github"
);
Wire it up — engine[] selects + configures it; plugins[] loads the artifact:
governance:
policy:
engine:
- kind: cedar
config:
policy_dir: "./policies/"
reload:
enabled: true
check_interval_sec: 30
plugins:
- id: dev.mcpg.policy.cedar
class: policy_engine
source:
oci: "ghcr.io/mcpg-dev/source-code/plugins/policy-cedar:1.0.0"
The kind: cedar alias resolves to dev.mcpg.policy.cedar. The plugin walks policy_dir
recursively at boot, aggregates every .cedar file into one PolicySet, and (with reload)
hot-swaps on change.
OPA (Rego)
If you already have Rego policies, use OPA. Two modes:
Embedded — compile your bundle and run it in-process, no external OPA daemon:
governance:
policy:
engine:
- kind: opa
config:
mode: embedded
policy_bundle:
source_path: "./bundle.tar.gz"
entrypoint: "mcpg/allow"
plugins:
- id: dev.mcpg.policy.opa
class: policy_engine
source:
oci: "ghcr.io/mcpg-dev/source-code/plugins/policy-opa:1.0.0"
Remote — talk to a separate OPA service:
governance:
policy:
engine:
- kind: opa
config:
mode: remote
remote:
url: "http://opa.svc.cluster.local:8181"
package: "mcpg/allow"
timeout_ms: 50
Casbin
When your model is fundamentally RBAC or ABAC and you want explainable denies:
governance:
policy:
engine:
- kind: casbin
config:
model_path: "./model.conf"
policy_path: "./policy.csv"
plugins:
- id: dev.mcpg.policy.casbin
class: policy_engine
source:
oci: "ghcr.io/mcpg-dev/source-code/plugins/policy-casbin:1.0.0"
Composition
Run multiple engines in one chain — list them in order:
governance:
policy:
engine:
- kind: cedar # fast structural check
config:
policy_dir: "./cedar/"
- kind: opa # data-heavy lookup against an external policy store
config:
mode: remote
remote:
url: "http://opa.svc.cluster.local:8181"
package: "mcpg/allow"
The host walks the chain in order. Cedar runs first; if it returns NotApplicable, OPA is
consulted next. Load both plugins under plugins[].
Per-binding gating
Tool-level governance lives on the binding's own governance: block — a minimum-trust
floor and a CEL predicate, applied alongside the global policy:
mcp:
capabilities:
tools:
- name: deploy.rollback
description: Roll back a production deployment.
governance:
minimum_trust: verified
allow_if: '"platform" in identity.groups'
backend:
kind: http
url: "https://deploy.internal.example.com/v1/deployments/${arguments.id}/rollback"
method: post
expected_status_codes: [200, 202]
The binding's allow_if and minimum_trust run pre-dispatch, in addition to the global
governance.policy chain — both must pass.
Audit
Every decision (permit, deny, with-obligations) writes an audit row. The audit ledger is chained with signatures so tampering is detectable. See the observability guide for the audit storage model.