Skip to main content
The policy layer decides whether a tool call is allowed. It is the first gate every governed action passes through. The design goal is safety under uncertainty: when a rule is missing, ambiguous, or errors out, the call is denied, never allowed. The engine implements Cedar’s evaluation semantics in pure Python, so it works on a zero-dependency install. Two ideas define its behavior:
  • Deny-by-default. Nothing is permitted unless a rule permits it.
  • Forbid-overrides-permit. A single matching forbid rule vetoes any number of permit rules.
The PolicyEngine protocol is a seam. The default PythonPolicyEngine can be swapped for an embedded Cedar engine later without changing any call site.

The Policy object

Policy is the ergonomic surface you write. It compiles to low-level rules and satisfies the PolicyEngine protocol, so you can pass it anywhere an engine is expected. It has four fields, all optional.

Allowlist vs permit-all semantics

The allow field flips the engine between two modes.
When allow is None, the engine emits a permit-all rule. Every tool is permitted unless it appears in deny. Use this for a permissive base where you subtract specific dangerous tools.
In allowlist mode, require_approval and approve_when can never widen the permit set. Approval only gates tools that are also allowed. A tool listed in require_approval but absent from allow stays denied, and approve_when is wrapped so it only matches allowlisted tools. Default-deny always wins for non-allowlisted tools.

Forbid overrides permit

Evaluation runs in a fixed order inside PythonPolicyEngine.evaluate:
1

Collect matching forbids

If any forbid rule matches the request, evaluation stops and returns FORBID. No permit can rescue a call that a forbid matches.
2

Collect matching permits

If no forbid matched, gather matching permit rules. If any match, return PERMIT and merge their obligations (deduplicated and sorted).
3

Default deny

If nothing matched, return FORBID with reason default-deny.
This is why deny beats allow. Putting a tool in both lists still forbids it.

Fail-closed behavior

The entire evaluation body is wrapped in a try/except. If any rule condition raises (a buggy approve_when predicate, for example), the engine does not propagate the exception and does not fall through to permit. It returns a FORBID decision with reason fail-closed:<ExceptionType>.
A crashing policy is a denied action, not an open door. This is the safest possible default for an authorization boundary.

What the engine evaluates

Each call is an AuthRequest, evaluated at a chokepoint. Its fields are available to your approve_when predicate and to any custom rule condition.
Evaluation returns a Decision.
The reasons tuple traces which named rules fired (permit:allowlist, forbid:denylist, default-deny, fail-closed:KeyError), which makes decisions auditable.
The only shipped obligation is Obligation.REQUIRE_APPROVAL. Other obligations such as redaction are intentionally not shipped until they are enforced, because a declared-but-unenforced control is worse than no control.

The PolicyEngine seam

PolicyEngine is a runtime-checkable Protocol with a single method.
Both Policy and PythonPolicyEngine satisfy it. To go beyond the four-field Policy surface, build rules directly. Each Rule has an effect, an optional tools tuple (None matches any tool), an optional condition predicate (None matches unconditionally), obligations, and a name for audit reasons.
Any object with a matching evaluate method is a valid engine, so you can also supply your own implementation behind the same protocol.

Governance overview

How policy, risk, approval, and audit compose on the agent loop.

Approval

Turning a REQUIRE_APPROVAL obligation into a human decision, including durable approval.