« Phase 09 · Warmup · Track Overview

Core Contributor — Working on the Engines Themselves

What it takes to contribute to OPA, Cedar, OpenFGA, or the PDP your bank builds in-house. Read this if you want to understand the systems rather than configure them.


Table of Contents


1. Why read the engines

Two practical reasons, beyond curiosity.

Performance debugging. When a Rego policy takes 40 ms, the fix follows from knowing how evaluation works — usually a comprehension in a hot rule that should be an indexed lookup. Without that model you are guessing.

Semantic corners. "What does OPA return when a rule is undefined?" and "does Cedar's forbid beat a permit in a different policy set?" are questions whose answers are your security boundary. They are documented, and they are also worth confirming in the source.

2. OPA: the architecture

   REST / gRPC / Go API
        │
   ┌────▼───────────────────────────────────────────────┐
   │  Compiler        parse → AST → type check → plan   │
   │  Topdown         the evaluator (topdown/)          │
   │  Storage         inmem store: policy + data        │
   │  Plugins         bundle, decision_logs, status     │
   └────────────────────────────────────────────────────┘

The pieces worth knowing by name in the repo:

PackageDoes
ast/parser, AST, compiler, the term representation
topdown/the evaluator — start here for semantics questions
storage/inmem/the document store, with transactions
plugins/bundle/download, verify, activate
rego/the embedding API most integrations use
ir/the intermediate representation, feeding Wasm and the planner

Bundles are activated under a write transaction against the store, which is what makes activation atomic from a query's point of view. Reading that code is the fastest way to understand why "atomic activation" is a real guarantee and not a slogan.

3. Rego evaluation

Rego is Datalog-descended, and the two consequences that matter:

Rules are sets of bindings, not functions. A rule body with unbound variables enumerates every binding that satisfies it. This is why

deny if {
    some tool in input.requested_tools
    tool.classification == "restricted"
}

reads as "there exists" rather than "loop and check" — and why an accidental unbound variable turns a cheap rule into a cross-product.

Undefined is not false. A rule whose body fails is undefined, and undefined propagates. This is the single most common source of a silent security hole:

# BROKEN: if input.user is missing, `allow` is undefined, not false
allow if { input.user.role == "admin" }

# CORRECT
default allow := false
allow if { input.user.role == "admin" }

Without the default, the caller receives {} rather than {"allow": false}, and whether that denies depends entirely on how the PEP reads the result. Requiring a default for every decision rule is a lint rule worth enforcing in CI.

Indexing. OPA builds a rule index over equality expressions on input, so input.action == "crm.read" in a rule body is a hash lookup, not a scan. Structure hot policies so the discriminating comparison is a top-level equality on input — the difference between an indexed and a scanned rule set is an order of magnitude.

4. Partial evaluation

The idea that makes OPA fast at scale: given part of the input, specialize the policy and return a residual policy over the rest.

   full policy  +  {"subject": {"tenant": "wholesale", "clearance": "confidential"}}
        │
        ▼  partial evaluation
   residual policy — only the rules that could still apply,
                     with the known parts folded away

Two uses:

Precompilation. Specialize on the parts of the input known at startup (region, environment, service identity) and evaluate the much smaller residual per request.

Data filtering. This is the powerful one. Instead of "may this principal read document 42?", ask "what is the condition under which this principal may read any document?" and get back a residual that compiles to a SQL WHERE clause. Now authorization is a predicate pushed into the query rather than a filter applied to results you already fetched — which is the difference between paginating correctly and not.

Implemented in topdown/save.go and the rego package's Partial API. If you want one thing to read in OPA, read this.

5. Cedar: the analyzability bet

Cedar (Rust, cedar-policy/cedar) makes a deliberate trade: less expressive, so it can be reasoned about mechanically.

The structure:

   permit (principal, action, resource) when { ... } unless { ... };
   forbid (principal, action, resource) when { ... };
  • forbid always wins. Deny-overrides is in the language, not in your rule-writing discipline.
  • The scope (principal, action, resource) is constrained syntactically, which is what makes slicing cheap.
  • Conditions are total and terminating — no unbounded loops, no recursion.

That last property is what buys automated reasoning: Cedar policies compile to SMT formulas, so a solver can answer questions no test suite can.

QuestionAnswerable
Are these two policy sets equivalent?
Does this change grant anything new?
Can any principal reach this resource?
Is this policy set ever satisfiable?

"Does this change grant anything new?" is the one to care about in a bank. A test suite tells you the cases you thought of; the solver tells you about the ones you did not. If you are choosing a policy language for a regulated platform, this is the argument.

Worth reading: cedar-policy-validator/ (the type system) and the cedar-lean formalization — the core semantics are proved in Lean, which is a rare thing for an authorization engine and the reason the guarantees are trustworthy.

6. Zanzibar and OpenFGA

Google's Zanzibar answers a different question: not "what attributes does this subject have?" but "is there a relationship path from this subject to this resource?"

   document:budget-2026#viewer@group:finance#member
   group:finance#member@user:layla
   ⇒ layla can view budget-2026

The hard parts, and why the paper is worth reading:

Zookies. A consistency token. "Evaluate this check against a snapshot at least as fresh as the one that produced this token" — which is how Zanzibar offers strong consistency where it matters and cheap stale reads everywhere else. It is the cleanest treatment of the freshness/availability trade-off in this phase, from a system that runs at Google scale.

Leopard. An index for deeply nested group expansion, because the naive recursive check is too slow for real group hierarchies.

For an agent platform, ReBAC is the right model for data authorization (which documents may this user see?) and ABAC is the right model for action authorization (may this agent release a payment?). Most banks need both, and the integration point is that a ReBAC check becomes an attribute in the ABAC decision.

7. Building an in-house PDP

Sometimes correct — a thin PDP over a well-chosen rule model, embedded, with your own bundle distribution. What it takes to be respectable:

Semantics first, written down. Default-deny, deny-overrides, all-facets-conjunctive, and what "undefined" means. Write it as a document before code. Every ambiguity you leave becomes a security question later.

A conformance suite. Requests in, expected decisions out, as data. It outlives every refactor and it is how you keep semantics stable across a rewrite.

Determinism. The same request against the same bundle must give the same decision, including which rule is named. Sort. Never iterate a set where order reaches the output.

Property tests. The invariants are unusually well suited to it:

# adding a DENY rule can never turn a DENY into an ALLOW
# adding an ALLOW rule can never turn an ALLOW into a DENY
# permuting the rule order never changes the effect
# the empty bundle denies everything

Hypothesis will find the case where your combining logic is subtly order-dependent, and it will find it in minutes.

A decision log from day one. Not an afterthought. It is the artifact everything downstream consumes, and its schema is much harder to change later than to get right now.

Explain mode. Given a request, why did it decide that? Which rules matched, which conditions failed and why. Without it, every policy question becomes a bisect, and the people who need answers are the ones least able to bisect.

8. Testing a policy engine

Five layers, in increasing value and decreasing frequency:

LayerCatches
Unit tests per rulethe rule does what its author meant
Property tests over the combining logicorder dependence, monotonicity violations
Conformance suitesemantic drift across refactors
Corpus replaythe empty-facet rule; every unintended difference
Shadow evaluation in productionwhat the corpus did not contain

Corpus replay deserves the emphasis. Keep a redacted sample of real decision inputs; replay the candidate bundle; diff. Every difference is intended or a bug. It is the only test that reliably catches an over-broad rule, because the failure mode of an over-broad rule is that nothing breaks.

And the test everyone forgets: the bundle-rejection paths. Unsigned, mis-signed, malformed, older-than-active. Those paths run during an incident, at 3 a.m., and if they are wrong the failure is that your policy silently reverts.

9. Contributing

OPA (open-policy-agent/opa) — Go, CNCF-graduated, active. Good entry points: builtin functions in topdown/ (self-contained, well-specified), performance work in the evaluator, rego playground fixtures. Read docs/ and the ADRs first; semantic changes need an RFC.

Cedar (cedar-policy/cedar) — Rust, smaller, unusually rigorous. Changes to the language need a change to the Lean proofs, which is a high bar and exactly why the guarantees hold. Entry points: the validator, error messages, language bindings.

OpenFGA (openfga/openfga) — Go, CNCF sandbox, friendlier to first-time contributors. Entry points: storage adapters, the modelling language, docs.

For all three the useful preparation is the same: implement the semantics yourself first — the lab is a small version of exactly that — then read theirs and find every place they differ. The differences are where the real engineering is.