Policy as Code (Cedar)
Lucid uses Cedar as the unified policy language for all enforcement decisions. Cedar replaces the previous Lucid Policy Language (LPL), OPA/Rego integrations, and block_on_* environment variable flags.
Every enforcement decision in Lucid flows through a single Cedar policy evaluated at the Gateway. ClaimsAuditors produce observations; Cedar policies define what those observations mean.
Why Cedar?
Cedar is an open-source policy language created by AWS, designed for authorization decisions. It was chosen for Lucid because:
| Concern | Cedar Solution |
|---|---|
| Readability | English-like permit/forbid syntax readable by non-engineers |
| Performance | Sub-millisecond evaluation, formally verified engine |
| Composability | Policies compose with deny-overrides (like AWS SCPs) |
| Scoping | Natural principal/resource hierarchy for org -> workspace -> agent |
| Tooling | Three-tab editor (IFTTT, Visual, Cedar) for different skill levels |
| Auditability | Each decision includes the specific policies that contributed |
Cedar Policy Basics
A Cedar policy has two forms:
// PERMIT: Allow an action when conditions are met
permit(principal, action, resource)
when { <conditions> };
// FORBID: Deny an action when conditions are met
forbid(principal, action, resource)
when { <conditions> };
In Lucid's context:
- principal = the user, agent, or API key making the request
- action = Action::"invoke" (calling the AI model)
- resource = the agent being invoked
- context = the ClaimsContext containing all auditor claims
ClaimsContext: Where Claims Meet Policy
When the Gateway evaluates a Cedar policy, it constructs a ClaimsContext from all auditor claims. Claim names are flattened with dots replaced by underscores:
| Claim Name | Cedar Context Path |
|---|---|
toxic_content |
context.claims.toxic_content |
injection_risk |
context.claims.injection_risk |
pii_types |
context.claims.pii_types |
detected_regions |
context.claims.detected_regions |
Example: Blocking Toxic Content
// Block requests where toxicity exceeds threshold
forbid(principal, action == Action::"invoke", resource)
when { context.claims.toxic_content > 0.8 };
Example: Requiring PII Clearance
// Block requests containing PII unless the agent has PII access
forbid(principal, action == Action::"invoke", resource)
when { context.claims.pii_types != [] }
unless { resource.has_pii_access == true };
Example: Data Sovereignty
// Block if request originates outside allowed regions
forbid(principal, action == Action::"invoke", resource)
when { !(context.claims.detected_regions.containsAny(resource.allowed_regions)) };
Example: Combined Safety Policy
// Block injection attempts
forbid(principal, action == Action::"invoke", resource)
when {
context.claims.injection_risk > 0.7
};
// Block toxic content
forbid(principal, action == Action::"invoke", resource)
when { context.claims.toxic_content > 0.7 };
// Block if PII detected and not authorized
forbid(principal, action == Action::"invoke", resource)
when { context.claims.pii_count > 0 }
unless { resource.pii_authorized == true };
// Default: allow everything else
permit(principal, action == Action::"invoke", resource);
Policy Scoping: Org -> Workspace -> Agent
Cedar policies are scoped hierarchically. Higher-level policies cannot be overridden by lower levels (deny-overrides).
Organization Level
Org-wide policies apply to all workspaces and agents. These are typically security baselines.
// Org policy: Block all prompt injection across the organization
@annotation("scope", "org")
@annotation("id", "org-injection-block")
forbid(principal, action == Action::"invoke", resource)
when { context.claims.injection_risk > 0.7 };
Workspace Level
Workspace policies apply to all agents in a workspace. They can add restrictions but cannot relax org policies.
// Workspace policy: Stricter toxicity threshold for customer-facing agents
@annotation("scope", "workspace")
@annotation("workspace_id", "ws-customer-support")
forbid(principal, action == Action::"invoke", resource)
when { context.claims.toxic_content > 0.5 };
Agent Level
Agent-specific policies. Cannot override org or workspace forbid rules.
// Agent policy: This specific agent requires location verification
@annotation("scope", "agent")
@annotation("agent_id", "agent-legal-reviewer")
forbid(principal, action == Action::"invoke", resource)
when { context.claims.location_confidence < 0.5 };
How Scoping Works
Org forbid rules (cannot be removed)
+ Workspace forbid rules (cannot remove org rules, can add more)
+ Agent forbid rules (cannot remove org/workspace rules, can add more)
+ Agent permit rules (can only permit what org/workspace allow)
= Final effective policy
This follows Cedar's deny-overrides semantics: if any forbid matches, the request is denied regardless of any permit rules.
Decision Annotations
Cedar policies can include annotations that control how the Gateway handles the decision:
// @decision: deny -- block the request
@annotation("decision", "deny")
forbid(principal, action == Action::"invoke", resource)
when { context.claims.injection_risk > 0.7 };
// @decision: warn -- allow but flag in passport
@annotation("decision", "warn")
forbid(principal, action == Action::"invoke", resource)
when { context.claims.toxic_content > 0.5 && context.claims.toxic_content <= 0.8 };
// @decision: escalate -- block and require human approval
@annotation("decision", "escalate")
forbid(principal, action == Action::"invoke", resource)
when { context.claims.pii_count > 5 };
// @decision: shadow -- evaluate but don't enforce (for testing)
@annotation("decision", "shadow")
forbid(principal, action == Action::"invoke", resource)
when { context.claims.new_experimental_check == true };
| Decision | Behavior |
|---|---|
deny |
Block the request (default for forbid) |
warn |
Allow but flag violation in passport |
escalate |
Block and create approval request for human review |
shadow |
Evaluate and log but do not enforce |
log |
Silent logging, always proceeds |
Relationship to Detection Settings
Detection settings and Cedar response rules live together in one AuditorPolicy document. Detection overrides (via AuditorPolicy.detection) control how sensitively auditors scan; Cedar rules define what to do with the resulting claims:
Detection Overrides → Claims → Cedar Response Rules
"How sensitively to scan" "What was observed" "What to do about it"
For example, the LLM judge auditor's detection overrides might set injection_threshold: 0.85, which controls the sensitivity of the injection scanner. The Cedar rules in the same policy then define what happens when the resulting injection_risk claim exceeds a threshold:
// Deny high-confidence injections
@annotation("decision", "deny")
forbid(principal, action == Action::"invoke", resource)
when { context.claims.injection_risk > 0.85 };
// Warn on moderate-confidence injections
@annotation("decision", "warn")
forbid(principal, action == Action::"invoke", resource)
when { context.claims.injection_risk > 0.60 };
Both detection overrides and Cedar rules can be configured via presets (Starter, Balanced, Strict) -- policy templates that bundle coherent detection and response configuration for common risk profiles.
Enforcement Modes for Detection Overrides
While Cedar policies use deny-overrides for scoping (org forbid cannot be overridden), detection overrides in AuditorPolicy.detection use a more granular enforcement model. Each field can carry an enforcement mode at the policy scope:
| Mode | Behavior | Example |
|---|---|---|
floor |
Override can raise but not lower | injection_threshold >= 0.7 |
ceiling |
Override can lower but not raise | max_tool_calls <= 50 |
exact |
Override must use specified value | compliance_mode = "hipaa" |
superset |
Override must include all specified items | pii_types >= [SSN, CREDIT_CARD] |
unlocked |
No constraint | log_level |
This enables org-level policies to set security baselines (e.g., "injection threshold must be at least 0.7") while allowing child policies to tighten them further. See the Configuration Reference for details.
Agent Identity in Cedar
When agents make requests, their identity is available as Cedar principals:
// Only the legal-reviewer agent can access compliance endpoints
permit(
principal == Agent::"legal-reviewer",
action == Action::"access_data",
resource
)
when { resource.service == "compliance-api" };
SPIFFE Identity
Agent SPIFFE IDs are available as principal attributes:
permit(principal, action == Action::"access_data", resource)
when {
principal.spiffe_id == "spiffe://lucid.ai/agent/legal-reviewer" &&
resource.service == "compliance-api"
};
OBO Delegation
For on-behalf-of requests, both the agent and delegating user are available in context:
// Agent can only access Slack on behalf of engineering team members
permit(principal, action == Action::"access_data", resource)
when {
context.act.sub == "spiffe://lucid.ai/agent/slack-assistant" &&
resource.service == "slack" &&
principal in Group::"engineering"
};
Using the Policy Editor
The Observer UI provides a three-tab policy editor for creating and managing Cedar policies.
IFTTT Mode
For non-technical users. Point-and-click rule builder:
IF toxic_content > 0.8 THEN block
IF injection_risk > 0.7 THEN block
IF pii_types contains "SSN" THEN escalate
Visual Mode
Drag-and-drop policy builder with claim autocomplete from auditor vocabularies.
Cedar Mode
Direct Cedar syntax editing with syntax highlighting, validation, and claim autocomplete.
All three modes produce the same Cedar output. Changes in any mode are reflected in the others. See the Policy Editor Guide for details.
Dynamic Policy Updates
Cedar policies can be updated without redeploying auditors or the Gateway. The Gateway polls for policy updates:
- Admin edits policy in Observer UI (or pushes via CLI)
- Policy is validated against the Cedar schema
- Gateway picks up the new policy on next refresh interval (default: 60s)
- New requests are evaluated against the updated policy
No auditor restarts required. No downtime.
Integration with Observer UI
The Observer dashboard displays Cedar policy evaluation details for every request:
- Which policies were evaluated
- Which claims were referenced
- Which
forbid/permitrules matched - The final decision and contributing reasons
- Compliance framework mappings
Migration from LPL and OPA
If you are migrating from the previous policy approaches:
| Previous Approach | Cedar Equivalent |
|---|---|
INJECTION_BLOCK_ON_DETECTION=true |
forbid(...) when { context.claims.injection_risk > 0.7 }; |
TOXICITY_THRESHOLD=0.7 env var |
forbid(...) when { context.claims.toxic_content > 0.7 }; |
LPL rules: with action: deny |
Cedar forbid(...) with when clause |
LPL enforcement: block |
Cedar forbid(...) (default behavior) |
LPL enforcement: warn |
Cedar forbid(...) with @annotation("decision", "warn") |
LPL enforcement: shadow |
Cedar forbid(...) with @annotation("decision", "shadow") |
OPA Rego deny[msg] |
Cedar forbid(...) |
PolicyEngine.evaluate(claims) |
Gateway Cedar evaluation (no SDK-side engine) |
DynamicPolicyEngine refresh |
Gateway auto-refresh from Observer/API |
Policy bundles (.bundle.yaml) |
Multiple Cedar policies with scope annotations |