How to Configure PolicyEvaluator with Custom OPA/Cedar Policy Backends for Multi-Org Governance
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:
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:
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:
- The engine clones the supplied context and enriches it with protocol-specific facets.
- It scans native
PolicyRules for a match. If found, the rule's effect (allow/deny/review) wins immediately. - If no native rule matches, the engine builds a backend context guaranteeing the presence of
actionandtool_namekeys (policy.go:18-25). - It iterates over registered backends in insertion order (
policy.go:30-40). - Each backend's
Evaluatemethod returns aBackendDecisioncontainingAllowed,Decision, optionalReason, 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:
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
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
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
LoadRegoandLoadCedarinpolicy.go:72-79to register external policy backends with thePolicyEngine. - Configuration Patterns: Pass
OPAOptionsorCedarOptionsto configure execution mode (OPAAuto,OPARemote,CedarAuto), policy paths, and timeouts. - Multi-Org Isolation: Create separate
PolicyEngineinstances per tenant, each loading organization-specific policy files, stored in a concurrent map for request-time retrieval. - Safety Defaults: Both backends support
AllowBuiltinFallbackto 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.
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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →