# How to Configure PolicyEvaluator with Custom OPA/Cedar Policy Backends for Multi-Org Governance

> Configure PolicyEvaluator with custom OPA or Cedar policy backends for multi-org governance. Learn how to implement isolated per-tenant policy evaluation with the Agent Governance Toolkit.

- Repository: [Microsoft/agent-governance-toolkit](https://github.com/microsoft/agent-governance-toolkit)
- Tags: how-to-guide
- Published: 2026-05-29

---

**The Agent Governance Toolkit's `PolicyEngine` supports custom OPA (Rego) and Cedar policy backends through option structs (`OPAOptions`, `CedarOptions`) registered via `LoadRego` and `LoadCedar`, enabling isolated per-tenant policy evaluation with automatic CLI-to-mock fallbacks.**

The Microsoft Agent Governance Toolkit provides a flexible authorization framework where the `PolicyEngine` evaluates native policy rules first and delegates to external backends when no match exists. By configuring custom OPA and Cedar backends, organizations can implement multi-organization governance where each tenant maintains isolated policy sets while sharing the same runtime infrastructure.

## Understanding the Policy Engine Architecture

The **Policy Engine** (`agentmesh.PolicyEngine`) serves as the central decision-making component. It evaluates native `PolicyRule` objects first, and when no rule matches, iterates through registered external backends in order until a decision is reached.

The toolkit ships with two interchangeable backends:

- **OPA (Open Policy Agent)**: Evaluates Rego policies via remote HTTP, CLI, or built-in mock evaluator (`OPABackend`)
- **Cedar**: Evaluates Cedar DSL policies via CLI or built-in mock evaluator (`CedarBackend`)

Both backends implement the same interface and are registered with the engine through dedicated loader methods defined in `policy.go:72-79`.

## Configuring OPA and Cedar Backends

Backend configuration occurs through option structs that specify execution mode, policy paths, and fallback behavior.

### OPA Backend Configuration

Instantiate an OPA backend using `NewOPABackend` with an `OPAOptions` struct:

```go
b := NewOPABackend(OPAOptions{
    Mode:        OPAAuto,               // auto → CLI if present, else built-in mock
    RegoPath:    "/policies/allow.rego", // or supply RegoContent directly
    Package:     "myorg",
    Query:       "data.myorg.allow",
    Timeout:     3 * time.Second,
    AllowBuiltinFallback: true,          // enable safe fallback when CLI missing
})

```

According to `policy_backends.go:72-98`, the constructor parses the Rego file if `RegoContent` is empty, validates the OPA URL (rejecting non-HTTP schemes and SSRF-prone hosts), and sets sensible defaults. When `Mode` is set to `OPARemote`, the backend sends evaluation requests to the configured OPA server via POST to `/v1/data/...`.

### Cedar Backend Configuration

Configure Cedar policies using `NewCedarBackend` with `CedarOptions`:

```go
b := NewCedarBackend(CedarOptions{
    Mode:        CedarAuto,
    PolicyPath:  "/policies/allow.cedar",
    EntitiesPath: "/policies/entities.json", // optional
    Timeout:     4 * time.Second,
    AllowBuiltinFallback: true,
})

```

As implemented in `policy_backends.go:95-121`, the builder reads the policy file and optional entities file, logs any I/O errors, and defaults to the built-in evaluator when the Cedar CLI is unavailable.

## Evaluation Flow and Decision Logic

When `PolicyEngine.Evaluate(action, ctx)` is called, the execution follows a strict precedence order:

1. The engine clones the supplied context and enriches it with protocol-specific facets.
2. It scans native `PolicyRule`s for a match. If found, the rule's effect (allow/deny/review) wins immediately.
3. If no native rule matches, the engine builds a **backend context** guaranteeing the presence of `action` and `tool_name` keys (`policy.go:18-25`).
4. It iterates over registered backends in insertion order (`policy.go:30-40`).
5. Each backend's `Evaluate` method returns a `BackendDecision` containing `Allowed`, `Decision`, optional `Reason`, and raw result data.

Errors from any backend are treated as a deny (fail-closed) per `policy.go:32-35`. The decision from each backend is normalized (`normalizeBackendDecision` in `policy.go:22-30`), and the first non-allowing result terminates the loop, ensuring **all backends must allow** for a final `Allow` decision.

## Implementing Multi-Organization Governance

To support multiple organizations (tenants) with distinct policy sets, instantiate a separate `PolicyEngine` per tenant, each loading its own OPA/Cedar policies:

```go
func NewTenantEngine(orgID string) *PolicyEngine {
    pe := NewPolicyEngine(nil)

    // Load OPA policy for this organisation
    pe.LoadRego(OPAOptions{
        Mode:        OPAAuto,
        RegoPath:    fmt.Sprintf("/configs/%s/opa.rego", orgID),
        Package:     orgID,
        Query:       fmt.Sprintf("data.%s.allow", orgID),
        AllowBuiltinFallback: true,
    })

    // Load Cedar policy for this organisation (optional)
    pe.LoadCedar(CedarOptions{
        Mode:        CedarAuto,
        PolicyPath:  fmt.Sprintf("/configs/%s/cedar.cedar", orgID),
        EntitiesPath: fmt.Sprintf("/configs/%s/entities.json", orgID),
        AllowBuiltinFallback: true,
    })

    return pe
}

```

At request time, the dispatcher fetches the proper engine by tenant ID and calls `engine.Evaluate(action, ctx)`. Because each engine holds its own backend instances, policies are isolated per organization while sharing the same runtime code.

## Fallback Mechanisms and Safety Guarantees

**Auto mode** (`OPAAuto` / `CedarAuto`) prefers the CLI (which provides the most complete feature set) but falls back to the built-in mock evaluator when the CLI binary is missing, **if** `AllowBuiltinFallback` is true. This behavior is demonstrated in the tests (`policy_backends_test.go:84-92` for OPA and `policy_backends_test.go:45-53` for Cedar).

The mock evaluators implement a strict subset of each language and **fail closed** on unsupported constructs, guaranteeing security even when a fallback occurs.

## Complete Implementation Examples

### Registering Multiple Backends in a Single Engine

```go
pe := NewPolicyEngine(nil)

// OPA backend – use remote server for high-scale evaluation
pe.LoadRego(OPAOptions{
    Mode:   OPARemote,
    OPAURL: "https://opa.myorg.example:8181",
    Query:  "data.myorg.allow",
    Timeout: 2 * time.Second,
})

// Cedar backend – CLI fallback with builtin mock as safety net
pe.LoadCedar(CedarOptions{
    Mode:   CedarAuto,
    PolicyPath: "./policy.cedar",
    EntitiesPath: "./entities.json",
    AllowBuiltinFallback: true,
})

// Evaluate a request from tenant "acme":
ctx := map[string]interface{}{
    "agent_id": "user123",
    "tenant":   "acme",
    "tool_name": "data.read",
}
decision := pe.Evaluate("data.read", ctx)
fmt.Println("Decision:", decision) // Allow / Deny / Review …

```

### Per-Tenant Engine Caching with sync.Map

```go
var engines = sync.Map{} // map[string]*PolicyEngine

func GetEngine(org string) *PolicyEngine {
    if v, ok := engines.Load(org); ok {
        return v.(*PolicyEngine)
    }
    engine := NewTenantEngine(org)
    engines.Store(org, engine)
    return engine
}

// In an HTTP handler:
func evalHandler(w http.ResponseWriter, r *http.Request) {
    org := r.Header.Get("X-Org-ID")
    action := r.URL.Query().Get("action")
    ctx := map[string]interface{}{
        "agent_id": r.Header.Get("X-Agent-ID"),
        "tenant":   org,
    }
    decision := GetEngine(org).Evaluate(action, ctx)
    json.NewEncoder(w).Encode(map[string]string{"decision": string(decision)})
}

```

## Summary

- **Backend Registration**: Use `LoadRego` and `LoadCedar` in `policy.go:72-79` to register external policy backends with the `PolicyEngine`.
- **Configuration Patterns**: Pass `OPAOptions` or `CedarOptions` to configure execution mode (`OPAAuto`, `OPARemote`, `CedarAuto`), policy paths, and timeouts.
- **Multi-Org Isolation**: Create separate `PolicyEngine` instances per tenant, each loading organization-specific policy files, stored in a concurrent map for request-time retrieval.
- **Safety Defaults**: Both backends support `AllowBuiltinFallback` to ensure availability, with mock evaluators that fail closed on unsupported language features.
- **Evaluation Order**: Native rules evaluate first, followed by backends in registration order; all must allow for final authorization.

## Frequently Asked Questions

### What is the difference between OPAAuto and OPARemote modes?

`OPAAuto` automatically detects the OPA CLI and falls back to a built-in mock evaluator if unavailable, while `OPARemote` sends evaluation requests to a specified OPA server URL via HTTP POST. According to `policy_backends.go:93-118`, `OPARemote` validates URLs to prevent SSRF attacks by rejecting non-HTTP schemes.

### How does the PolicyEngine handle backend evaluation failures?

The engine implements fail-closed semantics where any backend error results in an immediate deny decision. As noted in `policy.go:32-35`, errors during backend evaluation terminate the evaluation loop and return a deny status, ensuring that policy engine unavailability does not result in unauthorized access.

### Can I use both OPA and Cedar backends simultaneously in the same engine?

Yes, the `PolicyEngine` supports multiple concurrent backends registered via sequential calls to `LoadRego` and `LoadCedar`. The engine evaluates backends in the order they were added, requiring all backends to return allow for a final authorization, as demonstrated in the reference example at [`examples/policy-opa-cedar/main.go`](https://github.com/microsoft/agent-governance-toolkit/blob/main/examples/policy-opa-cedar/main.go).

### How do I ensure complete policy isolation between organizations?

Instantiate a distinct `PolicyEngine` per organization using tenant-specific configuration paths, and store these instances in a concurrent map keyed by organization ID. Since each engine maintains its own backend instances and loaded policies, no memory is shared between tenants, guaranteeing isolation while maximizing code reuse.